mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 04:32:23 +00:00
Compare commits
409 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57b905862a | |||
| 51d3a2b77b | |||
| b2920996be | |||
| bdc5a7a01b | |||
| 1c6a9e2c49 | |||
| 8ffb77551f | |||
| 2d35869c41 | |||
| 359dab573b | |||
| 6e2edbf101 | |||
| 4c8f93d737 | |||
| 2789bc565a | |||
| a1e0b8858e | |||
| f075a6178b | |||
| 5fa385e1e0 | |||
| b57141a80c | |||
| d453adb7cf | |||
| c5a7a8680e | |||
| d4ccadb832 | |||
| cbf04546b8 | |||
| 37c0084874 | |||
| 875afd69d5 | |||
| 67f4ed00d0 | |||
| 10d30b0de7 | |||
| 984b1212a6 | |||
| bc72c927c1 | |||
| b8275f2802 | |||
| 267527f3fc | |||
| c5b4513f65 | |||
| 5da4fbb39c | |||
| 93639e9e50 | |||
| d76462ca42 | |||
| 722201a1af | |||
| 0a3dddef2f | |||
| 26434f0331 | |||
| f6c2e2c20d | |||
| 424a06f6c8 | |||
| 89c4cbeb05 | |||
| 7e13ed746e | |||
| 28d205b4a3 | |||
| dcc52fc4e4 | |||
| a9d785c28f | |||
| 54a3730c20 | |||
| 31d322a28c | |||
| 3b90086109 | |||
| 30b870b98d | |||
| b3e7975b83 | |||
| 8f7f6223de | |||
| 6c313dd25c | |||
| b718bd19bd | |||
| ab5ea4c853 | |||
| d11ec67129 | |||
| 10f894e2ea | |||
| 34bbc8f1ea | |||
| a213ab6760 | |||
| d694d8fcc2 | |||
| 5f2d451c5e | |||
| ab696b980a | |||
| 9f2df7fb9d | |||
| 9e5c3ff675 | |||
| a67108e6c1 | |||
| b1706f2eac | |||
| ac29e152bb | |||
| de690a385c | |||
| a973b62918 | |||
| 92426a640a | |||
| 93f8638a94 | |||
| 16b0b1c3a5 | |||
| 25fff3fdd0 | |||
| 0384de46a4 | |||
| 080f325557 | |||
| 86cc2bf39a | |||
| c7effa3f25 | |||
| be262eaef2 | |||
| 523e51c775 | |||
| 5410f14aa1 | |||
| 45b2411c2b | |||
| 9f12091c7f | |||
| bef5d3cca5 | |||
| 0f548eb93e | |||
| 374348bb81 | |||
| b42f6b1827 | |||
| addcf531f0 | |||
| 0ef683013a | |||
| 137a306269 | |||
| fc0ffd1c02 | |||
| 97e0c43702 | |||
| 8dde98f543 | |||
| 9962a0c905 | |||
| 4b56b34124 | |||
| f857c513c5 | |||
| 41df6410bd | |||
| dcbd6a925d | |||
| 3848a21136 | |||
| 8af37cb4ec | |||
| 44c28e9a4c | |||
| 677324927c | |||
| d212344c54 | |||
| 74da327803 | |||
| f021041b31 | |||
| c05d2df116 | |||
| 2b926f7906 | |||
| 3ac69043d6 | |||
| f726708815 | |||
| 21e153087a | |||
| 31d85b715a | |||
| 344cc1fb7d | |||
| 1cea3d21e8 | |||
| 640320f70d | |||
| 29352092aa | |||
| 5b877b549a | |||
| 721466c429 | |||
| cce2fee1fa | |||
| 087d55f02a | |||
| f1037de14d | |||
| 4711672170 | |||
| c0b4e099af | |||
| f41fbf24c3 | |||
| e14d96e6ee | |||
| 716a33f347 | |||
| c5f084a4af | |||
| 606a0b46f2 | |||
| 1c729a74d1 | |||
| db31574103 | |||
| d03b47e1e6 | |||
| ae62ac2777 | |||
| 998f9c6a1e | |||
| 3ff31b577c | |||
| cd013079f4 | |||
| b8a9674483 | |||
| 8beb1bef98 | |||
| 377676f881 | |||
| 4efe284338 | |||
| 34a4e3f37a | |||
| 889c9a9e9f | |||
| 9e79499532 | |||
| 589b98053e | |||
| fc5bb230a9 | |||
| 3ef823db23 | |||
| 60a70f5236 | |||
| 757a010172 | |||
| 447625bffd | |||
| 25edaa9b89 | |||
| 9da4215bac | |||
| 6ddf91fb52 | |||
| 01361cc8f2 | |||
| 28eb2cd628 | |||
| fa4239159d | |||
| ea6f0b054a | |||
| 3b637a0234 | |||
| 45dacc3612 | |||
| f93d79dce0 | |||
| 6f8dcc637f | |||
| a748dda378 | |||
| b158a83644 | |||
| 31cd20c61b | |||
| 0a542e6957 | |||
| 89d29cf133 | |||
| 6ee7502920 | |||
| 708ee7350b | |||
| 836293504c | |||
| b458aea6b9 | |||
| e1e169ece8 | |||
| aeea9dfb5e | |||
| 9a9be821d1 | |||
| c808382154 | |||
| 81608b0cee | |||
| 02ae838c45 | |||
| f1f8b3da6b | |||
| 8ac0d22a95 | |||
| 3b6f79cc75 | |||
| 1e41561fe8 | |||
| 23abf674f3 | |||
| ac64d0c400 | |||
| 0983f9e66d | |||
| 5725ed867f | |||
| 4174a78bb5 | |||
| 96b0a241d9 | |||
| ff76850a54 | |||
| 9954e073fa | |||
| 5da68721d6 | |||
| 097f44284f | |||
| 1f605484d9 | |||
| 81dcad6a14 | |||
| 03af1013a0 | |||
| 144831f9ee | |||
| d6e69bd6e9 | |||
| 7d6fd03eb4 | |||
| 75c854da01 | |||
| 01ddf8e769 | |||
| 0a9c9f2a28 | |||
| 919dcbe2d9 | |||
| 5e237a1670 | |||
| acf6628f23 | |||
| 8a62558575 | |||
| 0e0861c84b | |||
| 5a8209e2ad | |||
| 50b41f5af6 | |||
| fc6678f5ce | |||
| ee1183481f | |||
| 6ddf7dae01 | |||
| 8232c9fa24 | |||
| 07b6c95f54 | |||
| ca2f683af3 | |||
| eb46de9658 | |||
| 59cc9c0a16 | |||
| 32d2f1d629 | |||
| 38a73d6510 | |||
| 59b496d4b5 | |||
| e56aa45a82 | |||
| a3c99b8362 | |||
| e66ac3d7db | |||
| 6b48e8b4b9 | |||
| 3d243b5eee | |||
| fa0dbac914 | |||
| 49e7abb4dc | |||
| 5098550399 | |||
| e12c05eea0 | |||
| 39433fb5a5 | |||
| 1ea80016cd | |||
| 58b39d3926 | |||
| 946c868388 | |||
| df4943205a | |||
| ba4eb2a891 | |||
| 90bb2f0e16 | |||
| c2840d92c2 | |||
| 81ff9c168e | |||
| 239ab5b587 | |||
| 37a33d62d5 | |||
| 13c6fdd3c6 | |||
| e69597f72b | |||
| 9c633c8b6c | |||
| 4d906578c7 | |||
| de744dddda | |||
| 0a04b88a52 | |||
| bee1e21f0e | |||
| f5c5fba151 | |||
| 4f9badc063 | |||
| b61d870922 | |||
| 153389fe4a | |||
| 965a45d65b | |||
| 06b84cfa24 | |||
| 97db95085a | |||
| d1bcec7a6a | |||
| 7b46c6a443 | |||
| 6db0c14fa8 | |||
| d03f9237e2 | |||
| 24716a0089 | |||
| 437d19e1f5 | |||
| bd5fd3539a | |||
| bcc67ecb31 | |||
| c50810a24a | |||
| 7f9ce37a98 | |||
| ac23d08717 | |||
| a8211ed592 | |||
| e38f3beb91 | |||
| f14bf8557a | |||
| cd11d2de27 | |||
| d764c0af23 | |||
| d24e5df7c1 | |||
| de5075aafe | |||
| 9474d17867 | |||
| abe6dbdfc9 | |||
| 7ecec973dd | |||
| f34aef1ebf | |||
| 3253bdf7aa | |||
| 8e2f9d2bb5 | |||
| 7dd0d7601a | |||
| 5bd53b973f | |||
| 911cfe010e | |||
| 35dd03664a | |||
| c89b84ff57 | |||
| cda13af7a3 | |||
| 6e84eb0acc | |||
| b9ede5c739 | |||
| 44b980c1e0 | |||
| 08f4dea47f | |||
| 8f2a7be903 | |||
| 63b541917e | |||
| c8706bddc9 | |||
| e969d407d3 | |||
| 066b198ac9 | |||
| 5d6119e542 | |||
| 845e229a9c | |||
| cefa088b59 | |||
| 109787d869 | |||
| 78abca16e6 | |||
| 02aaf1b409 | |||
| 02e102f2d2 | |||
| ec0dcb7f8c | |||
| ba63ed81a1 | |||
| fb6660361d | |||
| ecae9943e2 | |||
| 104e896531 | |||
| 6555e56647 | |||
| f9a590a22c | |||
| ad4a1576d3 | |||
| c35c3f06b5 | |||
| fada808a2f | |||
| e221e6fa6a | |||
| 725dac04d0 | |||
| 5c365990b9 | |||
| 797e2e41a5 | |||
| 57fe40839b | |||
| b4c5ad1e35 | |||
| 635ddce93d | |||
| 24e807cac7 | |||
| 33ceba38a8 | |||
| 90ed1f9b28 | |||
| e431cb205c | |||
| 09bb45f5bc | |||
| 7087452cde | |||
| af4b58444d | |||
| 711423cae5 | |||
| eb8b6062b4 | |||
| 5ac96cb5b9 | |||
| c462fc9337 | |||
| 95f7a2bcde | |||
| 04cf9c870b | |||
| bf9bddc867 | |||
| 20a2fda066 | |||
| f03afb20f3 | |||
| e0df22ffec | |||
| 96302a5c07 | |||
| 38afce1628 | |||
| cd5afacc97 | |||
| c2623e5dce | |||
| 42ecd7e127 | |||
| c80a7cfcc9 | |||
| 6d8f1a3980 | |||
| 5c4c61bb15 | |||
| d8af8b58bb | |||
| 3c0405249b | |||
| 7f57649d10 | |||
| fbde23870e | |||
| 7b968030ca | |||
| b5d672ba42 | |||
| efa8da83ba | |||
| 64cf7237db | |||
| cf9a94a799 | |||
| 46c1a8c8ff | |||
| ac7f2aeb62 | |||
| 9378a763f3 | |||
| 0bc83fedae | |||
| f7c03db752 | |||
| b6b8c21741 | |||
| c8af51cfd2 | |||
| 77de3820e7 | |||
| adc5bea4e9 | |||
| 1eddcb9ee0 | |||
| eae542f90f | |||
| fab0d05279 | |||
| d2e495a69f | |||
| 02058efa4d | |||
| ed79cbb1cc | |||
| 0065c5fba8 | |||
| 1ab24e8362 | |||
| 232b4ec34d | |||
| 75e6147dbc | |||
| 8d7b293381 | |||
| 19c8afb652 | |||
| 84dcee6b36 | |||
| dadcf03f55 | |||
| 413886ac6b | |||
| 377f68c020 | |||
| b718a37db2 | |||
| e21043b038 | |||
| 395598d8e3 | |||
| 813518bcb5 | |||
| 4ce7a898c9 | |||
| 78f235ea7c | |||
| bda2f50f9d | |||
| 5e5ba5b3d4 | |||
| 22b0e9dcc5 | |||
| 68420d43ea | |||
| 924e337c90 | |||
| 04599a10d2 | |||
| 84c931bb5f | |||
| 572aedb3eb | |||
| 1db86c24e3 | |||
| 74adde3030 | |||
| f68e8f0e00 | |||
| 98386984f3 | |||
| 7b35ed97ca | |||
| b3302ed679 | |||
| e12f060af8 | |||
| 6d4f47a2ca | |||
| e49ab7e0cc | |||
| ccfe7ab80a | |||
| 7b95358975 | |||
| bcc50aaadc | |||
| 30f63b7d67 | |||
| ead13e23bc | |||
| 24257d2a85 | |||
| 5c4dbb924d | |||
| 88e1b3d2f9 | |||
| f67b80c6a6 | |||
| c7fd42f2cf | |||
| bed03bf83c | |||
| 1e49400e93 | |||
| 608c0dc05d | |||
| 921dbe9dae | |||
| a7e0837f96 | |||
| 071c80f845 | |||
| 06d823b9d8 | |||
| b15e33e5e9 | |||
| e6bef16c45 | |||
| 4829c4c02b | |||
| 3bf4a8ef52 | |||
| c95a47ca97 |
@@ -16,7 +16,7 @@ jobs:
|
||||
- run: echo ok go
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
path: build/assets/Ifc*
|
||||
|
||||
deliver:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
name: Docker Build, Tag, Push
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: ci-bsdd-pypi
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# * * * * *
|
||||
- cron: "0 0 18 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
major: 0
|
||||
minor: 0
|
||||
name: ifcopenshell
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/bsdd &&
|
||||
make dist
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: ortega2247/pypi-upload-action@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/bsdd/dist
|
||||
@@ -0,0 +1,48 @@
|
||||
name: ci-ifcclash-pypi
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# * * * * *
|
||||
- cron: "0 0 18 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
major: 0
|
||||
minor: 0
|
||||
name: ifcopenshell
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifcclash &&
|
||||
make dist
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: ortega2247/pypi-upload-action@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifcclash/dist
|
||||
@@ -0,0 +1,48 @@
|
||||
name: ci-ifccsv-pypi
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# * * * * *
|
||||
- cron: "0 0 18 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
major: 0
|
||||
minor: 0
|
||||
name: ifcopenshell
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifccsv &&
|
||||
make dist
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: ortega2247/pypi-upload-action@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifccsv/dist
|
||||
@@ -1,6 +1,7 @@
|
||||
name: ci-ifcopenshell-pypi
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py36, py37, py38, py39, py310, py311]
|
||||
pyver: [py39, py310, py311, py312]
|
||||
config:
|
||||
- {
|
||||
name: "Windows Build",
|
||||
@@ -57,7 +58,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
- run: echo ${{ env.DATE }}
|
||||
- name: Get current date
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: ci-ifcpatch-pypi
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# * * * * *
|
||||
- cron: "0 0 18 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
major: 0
|
||||
minor: 0
|
||||
name: ifcopenshell
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifcpatch &&
|
||||
make dist
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: ortega2247/pypi-upload-action@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifcpatch/dist
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Publish-ifcsverchok
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/ci-ifcsverchok-build.yml'
|
||||
- 'src/ifcsverchok/*'
|
||||
branches:
|
||||
- v0.7.0
|
||||
|
||||
env:
|
||||
major: 0
|
||||
minor: 0
|
||||
name: ifcsverchok
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
name: ifcsverchok
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
- run: echo ${{ env.DATE }}
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%y%m%d')"
|
||||
- name: Compile
|
||||
run: |
|
||||
cd src/ifcsverchok
|
||||
make dist
|
||||
- name: Upload Zip file to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: src/ifcsverchok/dist/ifcsverchok-${{steps.date.outputs.date}}.zip
|
||||
asset_name: ifcsverchok-${{steps.date.outputs.date}}.zip
|
||||
tag: "ifcsverchok-${{steps.date.outputs.date}}"
|
||||
overwrite: true
|
||||
body: "ifcsverchok build for ${{steps.date.outputs.date}}"
|
||||
@@ -9,9 +9,6 @@ on:
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# * * * * *
|
||||
- cron: "0 0 18 * *"
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/ci-ifctester-pypi.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -30,7 +27,6 @@ jobs:
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -38,12 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
- run: echo ${{ env.DATE }}
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%y%m%d')"
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
|
||||
@@ -18,14 +18,14 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil
|
||||
For more information, see:
|
||||
|
||||
* [IfcOpenShell Website](http://ifcopenshell.org)
|
||||
* [IfcOpenShell Documentation](http://blenderbim.org/docs-python)
|
||||
* [IfcOpenShell C++ Installation](https://blenderbim.org/docs-python/ifcopenshell/installation.html)
|
||||
* [IfcOpenShell Python Installation](https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
|
||||
* [IfcOpenShell Python Hello World Tutorial](https://blenderbim.org/docs-python/ifcopenshell-python/hello_world.html)
|
||||
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
|
||||
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
|
||||
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
|
||||
* [IfcOpenShell Python Hello World Tutorial](https://docs.ifcopenshell.org/ifcopenshell-python/hello_world.html)
|
||||
* [BlenderBIM Add-on Website](https://blenderbim.org)
|
||||
* [BlenderBIM Add-on Documentation](http://blenderbim.org/docs)
|
||||
* [Add-on Installation](https://blenderbim.org/docs/users/installation.html)
|
||||
* [Exploring an IFC model](https://blenderbim.org/docs/users/exploring_an_ifc_model.html)
|
||||
* [BlenderBIM Add-on Documentation](https://docs.blenderbim.org/index.html)
|
||||
* [Add-on Installation](https://docs.blenderbim.org/users/installation.html)
|
||||
* [Exploring an IFC model](https://docs.blenderbim.org/users/exploring_an_ifc_model.html)
|
||||
|
||||
| Service | Status |
|
||||
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN apt-get -y update && apt-get -y install unzip curl
|
||||
# Install AWS Lambda runtime interface client
|
||||
RUN pip install --target ${FUNCTION_DIR} awslambdaric
|
||||
|
||||
# Set the IfcOpenShell build version (check available builds at: https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
|
||||
# Set the IfcOpenShell build version (check available builds at: https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
|
||||
ARG IFC_OPENSHELL_BUILD="39-v0.7.0-476ab50"
|
||||
|
||||
# Download and extract IfcOpenShell
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<licenseUrl>https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/COPYING</licenseUrl>
|
||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||
<projectSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</projectSourceUrl>
|
||||
<docsUrl>https://blenderbim.org/docs/</docsUrl>
|
||||
<docsUrl>https://docs.blenderbim.org/</docsUrl>
|
||||
<!--<mailingListUrl></mailingListUrl>-->
|
||||
<bugTrackerUrl>https://github.com/IfcOpenShell/IfcOpenShell/issues</bugTrackerUrl>
|
||||
<tags>blender bim blenderbim ifc python opensource foss</tags>
|
||||
|
||||
@@ -137,7 +137,7 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
@@ -151,8 +151,10 @@ endif
|
||||
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.7.0.zip
|
||||
cd dist/working && unzip v0.7.0.zip
|
||||
# IfcOpenBot sometimes lags behind, so we hotfix the Python utilities
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/
|
||||
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/util/
|
||||
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/api/
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util dist/blenderbim/libs/site/packages/ifcopenshell/
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api dist/blenderbim/libs/site/packages/ifcopenshell/
|
||||
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/*.py dist/blenderbim/libs/site/packages/ifcopenshell/
|
||||
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py dist/blenderbim/libs/site/packages/ifcopenshell/express/
|
||||
# Provides bcf functionality
|
||||
@@ -362,7 +364,9 @@ endif
|
||||
# Provides express rule validation for ifcopenshell.validate
|
||||
cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages
|
||||
# Provides Brickschema functionality
|
||||
cd dist/working && . env/bin/activate && $(PIP) install brickschema[persistence] --target=./site-packages
|
||||
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
|
||||
# Required for SVG to DXF conversion
|
||||
cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages
|
||||
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
@@ -430,7 +434,7 @@ endif
|
||||
# Required by brickschema
|
||||
# This is a bit of a dodgy one, it should be provided by setuptools which Blender doesn't ship.
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://blenderbim.org/builds/pkg_resources.tar.gz
|
||||
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/raw/v0.7.0/src/blenderbim/scripts/pkg_resources.tar.gz
|
||||
cd dist/working && tar -xzvf pkg_resources*
|
||||
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
@@ -18,7 +18,13 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import site
|
||||
import bpy
|
||||
import platform
|
||||
import traceback
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import addon_utils
|
||||
from collections import deque
|
||||
|
||||
bl_info = {
|
||||
"name": "BlenderBIM",
|
||||
@@ -27,21 +33,155 @@ bl_info = {
|
||||
"blender": (3, 1, 0),
|
||||
"version": (0, 0, 999999),
|
||||
"location": "File Menu, Scene Properties Tab. See documentation for more.",
|
||||
"doc_url": "https://blenderbim.org/docs",
|
||||
"doc_url": "https://docs.blenderbim.org/",
|
||||
"tracker_url": "https://github.com/IfcOpenShell/IfcOpenShell/issues",
|
||||
"category": "System",
|
||||
}
|
||||
|
||||
last_error = None
|
||||
last_actions: deque = deque(maxlen=10)
|
||||
|
||||
|
||||
def get_debug_info():
|
||||
version = ".".join(
|
||||
[
|
||||
str(x)
|
||||
for x in [
|
||||
addon.bl_info.get("version", (-1, -1, -1))
|
||||
for addon in addon_utils.modules()
|
||||
if addon.bl_info["name"] == "BlenderBIM"
|
||||
][0]
|
||||
]
|
||||
)
|
||||
return {
|
||||
"os": platform.system(),
|
||||
"os_version": platform.version(),
|
||||
"python_version": platform.python_version(),
|
||||
"architecture": platform.architecture(),
|
||||
"machine": platform.machine(),
|
||||
"processor": platform.processor(),
|
||||
"blender_version": bpy.app.version_string,
|
||||
"blenderbim_version": version,
|
||||
"last_actions": last_actions,
|
||||
"last_error": last_error,
|
||||
}
|
||||
|
||||
|
||||
def format_debug_info(info: dict):
|
||||
last_actions = ""
|
||||
for action in info["last_actions"]:
|
||||
last_actions += f"\n# {action['type']}: {action['name']}"
|
||||
if settings := action.get("settings"):
|
||||
last_actions += f"\n>>> {settings}"
|
||||
info["last_actions"] = last_actions
|
||||
text = "\n".join(f"{k}: {v}" for k, v in info.items())
|
||||
return text.strip()
|
||||
|
||||
|
||||
if sys.modules.get("bpy", None):
|
||||
# Process *.pth in /libs/site/packages to setup globally importable modules
|
||||
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
|
||||
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
|
||||
|
||||
import blenderbim.bim
|
||||
try:
|
||||
import blenderbim.bim
|
||||
import ifcopenshell.api
|
||||
|
||||
def register():
|
||||
blenderbim.bim.register()
|
||||
def log_api(usecase_path, ifc_file, settings):
|
||||
last_actions.append(
|
||||
{
|
||||
"type": "ifcopenshell.api",
|
||||
"name": usecase_path,
|
||||
"settings": ifcopenshell.api.serialise_settings(settings),
|
||||
}
|
||||
)
|
||||
|
||||
def unregister():
|
||||
blenderbim.bim.unregister()
|
||||
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
|
||||
|
||||
def register():
|
||||
blenderbim.bim.register()
|
||||
|
||||
def unregister():
|
||||
blenderbim.bim.unregister()
|
||||
|
||||
except:
|
||||
last_error = traceback.format_exc()
|
||||
|
||||
print(last_error)
|
||||
print(format_debug_info(get_debug_info()))
|
||||
print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on")
|
||||
|
||||
class BIM_PT_fatal_error(bpy.types.Panel):
|
||||
bl_label = "BlenderBIM Fatal Error"
|
||||
bl_idname = "SCENE_PT_error_message"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="BlenderBIM could not load.", icon="ERROR")
|
||||
layout.label(text="View the console for full logs.", icon="CONSOLE")
|
||||
box = layout.box()
|
||||
info = get_debug_info()
|
||||
py = ".".join(info["python_version"].split(".")[0:2])
|
||||
b3d = ".".join(info["blender_version"].split(".")[0:2])
|
||||
box.label(text=f"Blender {b3d} {info['os']} {info['machine']}", icon="BLENDER")
|
||||
box.label(text=f"Python {py} BBIM {info['blenderbim_version']}", icon="SCRIPTPLUGINS")
|
||||
layout.operator("bim.copy_debug_information", text="Copy Error Message To Clipboard")
|
||||
op = layout.operator("bim.open_uri", text="How Can I Fix This?")
|
||||
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html#installation-issues"
|
||||
|
||||
class OpenUri(bpy.types.Operator):
|
||||
bl_idname = "bim.open_uri"
|
||||
bl_label = "Open URI"
|
||||
uri: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open(self.uri)
|
||||
return {"FINISHED"}
|
||||
|
||||
class CopyDebugInformation(bpy.types.Operator):
|
||||
bl_idname = "bim.copy_debug_information"
|
||||
bl_label = "Copy Debug Information"
|
||||
bl_description = "Copies debugging information to your clipboard for use in bugreports"
|
||||
|
||||
def execute(self, context):
|
||||
info = format_debug_info(get_debug_info())
|
||||
|
||||
if platform.system() == "Windows":
|
||||
command = "echo | set /p nul=" + info
|
||||
elif platform.system() == "Darwin": # for MacOS
|
||||
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
|
||||
else: # Linux
|
||||
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
|
||||
subprocess.run(command, shell=True, check=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
class HiddenPanel:
|
||||
@classmethod
|
||||
def false_poll(cls, context):
|
||||
return False
|
||||
|
||||
def register():
|
||||
# Only show our error panel and nothing else in the scene tab
|
||||
for item_name in dir(bpy.types):
|
||||
item = getattr(bpy.types, item_name)
|
||||
if not hasattr(item, "bl_rna") or not isinstance(item.bl_rna, bpy.types.Panel):
|
||||
continue
|
||||
if getattr(item, "bl_context", None) != "scene":
|
||||
continue
|
||||
|
||||
# Reregister scene panel with a new poll to hide it
|
||||
item.poll = HiddenPanel.false_poll
|
||||
bpy.utils.unregister_class(item)
|
||||
bpy.utils.register_class(item)
|
||||
bpy.utils.register_class(BIM_PT_fatal_error)
|
||||
bpy.utils.register_class(CopyDebugInformation)
|
||||
bpy.utils.register_class(OpenUri)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(OpenUri)
|
||||
bpy.utils.unregister_class(CopyDebugInformation)
|
||||
bpy.utils.unregister_class(BIM_PT_fatal_error)
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
import bpy.utils.previews
|
||||
import blenderbim
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from . import handler, ui, prop, operator, helper
|
||||
from typing import Callable, Union
|
||||
|
||||
try:
|
||||
from blenderbim.translations import translations_dict
|
||||
@@ -96,10 +97,13 @@ for name in modules.keys():
|
||||
classes = [
|
||||
operator.AddIfcFile,
|
||||
operator.BIM_OT_add_section_plane,
|
||||
operator.BIM_OT_delete_object,
|
||||
operator.BIM_OT_open_webbrowser,
|
||||
operator.BIM_OT_remove_section_plane,
|
||||
operator.BIM_OT_select_object,
|
||||
operator.BIM_OT_show_description,
|
||||
operator.ClippingPlaneCutWithCappings,
|
||||
operator.CloseError,
|
||||
operator.EditBlenderCollection,
|
||||
operator.FileAssociate,
|
||||
operator.FileUnassociate,
|
||||
@@ -130,6 +134,7 @@ classes = [
|
||||
prop.BIMMeshProperties,
|
||||
prop.BIMFacet,
|
||||
prop.BIMFilterGroup,
|
||||
ui.BIM_UL_clipping_plane,
|
||||
ui.BIM_UL_generic,
|
||||
ui.BIM_UL_topics,
|
||||
ui.BIM_ADDON_preferences,
|
||||
@@ -187,7 +192,7 @@ addon_keymaps = []
|
||||
icons = None
|
||||
is_registering = False
|
||||
last_commit_hash = "8888888"
|
||||
overridden_scene_panels = dict()
|
||||
original_scene_panels_polls: dict[bpy.types.Panel, Union[Callable, None]] = dict()
|
||||
|
||||
|
||||
def on_register(scene):
|
||||
@@ -295,10 +300,10 @@ def unregister():
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
for panel in tuple(overridden_scene_panels.keys()):
|
||||
original_panel, override_panel = overridden_scene_panels[panel]
|
||||
bpy.utils.unregister_class(override_panel)
|
||||
bpy.utils.register_class(original_panel)
|
||||
del overridden_scene_panels[panel]
|
||||
import blenderbim.tool as tool
|
||||
|
||||
# use tuple() as method will be removing keys from dict
|
||||
for panel in tuple(original_scene_panels_polls.keys()):
|
||||
tool.Blender.remove_scene_panel_override(panel)
|
||||
|
||||
bpy.app.translations.unregister("blenderbim")
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
@@ -35,6 +36,8 @@ import blenderbim.core.spatial
|
||||
import blenderbim.core.style
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from mathutils import Vector
|
||||
from typing import Union
|
||||
from logging import Logger
|
||||
|
||||
|
||||
class IfcExporter:
|
||||
@@ -86,8 +89,8 @@ class IfcExporter:
|
||||
self.get_application_name(), tool.Blender.get_blenderbim_version()
|
||||
)
|
||||
|
||||
def sync_all_objects(self):
|
||||
results = []
|
||||
def sync_all_objects(self, skip_unlinking=False) -> list[ifcopenshell.entity_instance]:
|
||||
results: list[ifcopenshell.entity_instance] = []
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
for ifc_definition_id in list(IfcStore.id_map.keys()):
|
||||
obj = IfcStore.id_map[ifc_definition_id]
|
||||
@@ -96,19 +99,21 @@ class IfcExporter:
|
||||
continue
|
||||
if obj.library:
|
||||
continue
|
||||
tool.Collector.sync(obj)
|
||||
tool.Collector.sync(obj, skip_unlinking)
|
||||
result = self.sync_object_placement(obj)
|
||||
if result:
|
||||
results.append(result)
|
||||
result = self.sync_object_material(obj)
|
||||
# TODO: sync_object_material always returns None
|
||||
# so it's never really appended
|
||||
if result:
|
||||
results.append(result)
|
||||
except ReferenceError:
|
||||
pass # The object is likely deleted
|
||||
return results
|
||||
|
||||
def sync_edited_objects(self):
|
||||
results = []
|
||||
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
|
||||
results: list[ifcopenshell.entity_instance] = []
|
||||
for obj in IfcStore.edited_objs.copy():
|
||||
if not obj:
|
||||
continue
|
||||
@@ -126,7 +131,7 @@ class IfcExporter:
|
||||
IfcStore.edited_objs.clear()
|
||||
return results
|
||||
|
||||
def sync_object_material(self, obj):
|
||||
def sync_object_material(self, obj: bpy.types.Object) -> None:
|
||||
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
|
||||
return
|
||||
if not self.has_changed_materials(obj):
|
||||
@@ -137,11 +142,10 @@ class IfcExporter:
|
||||
checksum = obj.data.BIMMeshProperties.material_checksum
|
||||
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
|
||||
|
||||
def sync_object_placement(self, obj):
|
||||
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if not tool.Ifc.is_moved(obj):
|
||||
return
|
||||
blender_matrix = np.array(obj.matrix_world)
|
||||
if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
return element
|
||||
@@ -152,7 +156,7 @@ class IfcExporter:
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
return element
|
||||
|
||||
def sync_grid_axis_object_placement(self, obj, element):
|
||||
def sync_grid_axis_object_placement(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
||||
grid_obj = tool.Ifc.get_object(grid)
|
||||
if grid_obj:
|
||||
@@ -161,10 +165,10 @@ class IfcExporter:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
def get_application_name(self):
|
||||
def get_application_name(self) -> str:
|
||||
return "BlenderBIM"
|
||||
|
||||
def get_application_version(self):
|
||||
def get_application_version(self) -> str:
|
||||
version = ".".join(
|
||||
[
|
||||
str(x)
|
||||
@@ -182,11 +186,13 @@ class IfcExporter:
|
||||
|
||||
class IfcExportSettings:
|
||||
def __init__(self):
|
||||
self.logger = None
|
||||
self.output_file = None
|
||||
self.logger: Logger = None
|
||||
self.output_file: str = None
|
||||
self.json_version: str = None
|
||||
self.json_compact: bool = None
|
||||
|
||||
@staticmethod
|
||||
def factory(context, output_file, logger):
|
||||
def factory(context: bpy.types.Context, output_file: str, logger: Logger) -> IfcExportSettings:
|
||||
settings = IfcExportSettings()
|
||||
settings.output_file = output_file
|
||||
settings.logger = logger
|
||||
|
||||
@@ -31,6 +31,7 @@ from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
|
||||
from mathutils import Vector
|
||||
from math import cos, degrees
|
||||
from typing import Union
|
||||
|
||||
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
@@ -191,6 +192,8 @@ 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
|
||||
|
||||
|
||||
@persistent
|
||||
def loadIfcStore(scene):
|
||||
@@ -220,7 +223,7 @@ def redo_post(scene):
|
||||
tool.Ifc.rebuild_element_maps()
|
||||
|
||||
|
||||
def get_application(ifc):
|
||||
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
|
||||
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
|
||||
version = get_application_version()
|
||||
for element in ifc.by_type("IfcApplication"):
|
||||
@@ -235,7 +238,20 @@ def get_application(ifc):
|
||||
)
|
||||
|
||||
|
||||
def get_application_version():
|
||||
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
|
||||
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
|
||||
if pao := next(iter(ifc.by_type("IfcPersonAndOrganization")), None):
|
||||
return pao
|
||||
elif ifc.schema == "IFC2X3":
|
||||
if (person := next(iter(ifc.by_type("IfcPerson")), None)) is None:
|
||||
person = tool.Ifc.run("owner.add_person")
|
||||
if (organization := next(iter(ifc.by_type("IfcOrganization")), None)) is None:
|
||||
organization = tool.Ifc.run("owner.add_organisation")
|
||||
pao = tool.Ifc.run("owner.add_person_and_organisation", person=person, organisation=organization)
|
||||
return pao
|
||||
|
||||
|
||||
def get_application_version() -> str:
|
||||
return ".".join(
|
||||
[
|
||||
str(x)
|
||||
@@ -254,60 +270,6 @@ def viewport_shading_changed_callback(area):
|
||||
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll_check_blender_tab(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
|
||||
def get_override_scene_panel(panel_name):
|
||||
original_panel = getattr(bpy.types, panel_name)
|
||||
|
||||
override_panel = type(f"Override_{panel_name}", (original_panel,), {})
|
||||
override_panel.bl_idname = f"{panel_name}_override"
|
||||
|
||||
# some blender panel depend on other and register_class will throw an error
|
||||
# if we won't override their `bl_parent_id`
|
||||
bl_parent_id = getattr(override_panel, "bl_parent_id", None)
|
||||
if bl_parent_id is not None:
|
||||
override_panel.bl_parent_id = f"{bl_parent_id}_override"
|
||||
|
||||
# override poll method
|
||||
poll = getattr(override_panel, "poll", None)
|
||||
if poll is None:
|
||||
override_panel.poll = poll_check_blender_tab
|
||||
else:
|
||||
|
||||
@classmethod
|
||||
def wrapped_poll(cls, context):
|
||||
return super(override_panel, cls).poll(context) and poll_check_blender_tab.__func__(cls, context)
|
||||
|
||||
override_panel.poll = wrapped_poll
|
||||
|
||||
return override_panel
|
||||
|
||||
|
||||
# TODO: possibly override scene panels from other addons too?
|
||||
# list can be updated from
|
||||
# https://projects.blender.org/blender/blender/src/branch/main/scripts/startup/bl_ui/properties_scene.py#L421
|
||||
OVERRIDE_SCENE_PANELS = (
|
||||
"SCENE_PT_scene",
|
||||
"SCENE_PT_unit",
|
||||
"SCENE_PT_physics",
|
||||
"SCENE_PT_rigid_body_world",
|
||||
"SCENE_PT_rigid_body_world_settings",
|
||||
"SCENE_PT_rigid_body_cache",
|
||||
"SCENE_PT_rigid_body_field_weights",
|
||||
"SCENE_PT_audio",
|
||||
"SCENE_PT_keying_sets",
|
||||
"SCENE_PT_custom_props",
|
||||
# after SCENE_PT_keying_sets
|
||||
"SCENE_PT_keying_set_paths",
|
||||
"SCENE_PT_keyframing_settings",
|
||||
)
|
||||
if bpy.app.version >= (4, 0):
|
||||
OVERRIDE_SCENE_PANELS += ("SCENE_PT_simulation",)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post(scene):
|
||||
global global_subscription_owner
|
||||
@@ -330,7 +292,7 @@ def load_post(scene):
|
||||
key=key, owner=global_subscription_owner, args=(area,), notify=viewport_shading_changed_callback
|
||||
)
|
||||
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
|
||||
ifcopenshell.api.owner.settings.get_user = get_user
|
||||
ifcopenshell.api.owner.settings.get_application = get_application
|
||||
AuthoringData.type_thumbnails = {}
|
||||
|
||||
@@ -343,15 +305,11 @@ def load_post(scene):
|
||||
else:
|
||||
bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend"))
|
||||
|
||||
# To improve usability for new users, we hijack the scene properties
|
||||
# tab. We override default scene properties panels with our own poll
|
||||
# to hide them unless the user has chosen to view Blender properties.
|
||||
for panel in OVERRIDE_SCENE_PANELS:
|
||||
if panel in blenderbim.bim.overridden_scene_panels:
|
||||
continue
|
||||
override_panel = get_override_scene_panel(panel)
|
||||
original_panel = getattr(bpy.types, panel)
|
||||
bpy.utils.register_class(override_panel)
|
||||
bpy.utils.unregister_class(original_panel)
|
||||
blenderbim.bim.overridden_scene_panels[panel] = (original_panel, override_panel)
|
||||
# To improve usability for new users, we hijack the scene properties
|
||||
# tab. We override default scene properties panels with our own poll
|
||||
# to hide them unless the user has chosen to view Blender properties.
|
||||
for panel in tool.Blender.get_scene_panels_list():
|
||||
if panel in blenderbim.bim.original_scene_panels_polls:
|
||||
continue
|
||||
tool.Blender.override_scene_panel(panel)
|
||||
tool.Blender.setup_tabs()
|
||||
|
||||
@@ -29,6 +29,7 @@ from mathutils import geometry
|
||||
from mathutils import Vector
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from typing import Optional, Callable, Any
|
||||
|
||||
|
||||
def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None):
|
||||
@@ -75,6 +76,10 @@ def draw_attribute(attribute, layout, copy_operator=None):
|
||||
op.data_path = attribute.path_from_id("string_value")
|
||||
if attribute.is_optional:
|
||||
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
|
||||
if attribute.name == "GlobalId":
|
||||
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
|
||||
|
||||
if copy_operator:
|
||||
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
|
||||
op.name = attribute.name
|
||||
@@ -116,7 +121,7 @@ def import_attribute(attribute, props, data, callback=None):
|
||||
elif is_handled_by_callback is False:
|
||||
props.remove(len(props) - 1)
|
||||
elif data_type == "string":
|
||||
new.string_value = "" if new.is_null else str(data[attribute.name()])
|
||||
new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n")
|
||||
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
|
||||
new.is_uri = True
|
||||
elif data_type == "boolean":
|
||||
@@ -178,7 +183,7 @@ def add_attribute_description(attribute_blender, attribute_ifc=None):
|
||||
attribute_blender.description = description
|
||||
|
||||
|
||||
def export_attributes(props, callback=None):
|
||||
def export_attributes(props, callback: Optional[Callable] = None) -> dict[str, Any]:
|
||||
attributes = {}
|
||||
for prop in props:
|
||||
is_handled_by_callback = callback(attributes, prop) if callback else False
|
||||
@@ -271,6 +276,9 @@ def draw_filter(layout, filter_groups, data, module):
|
||||
op.type = sprops.facet
|
||||
op.index = i
|
||||
op.module = module
|
||||
op = row.operator("bim.remove_filter_group", text="", icon="X")
|
||||
op.index = i
|
||||
op.module = module
|
||||
|
||||
for j, ifc_filter in enumerate(filter_group.filters):
|
||||
if ifc_filter.type == "entity":
|
||||
@@ -317,11 +325,6 @@ def draw_filter(layout, filter_groups, data, module):
|
||||
op.index = j
|
||||
op.module = module
|
||||
|
||||
row = box.row()
|
||||
op = row.operator("bim.remove_filter_group", icon="X")
|
||||
op.index = i
|
||||
op.module = module
|
||||
|
||||
|
||||
# TODO this should move into ifcopenshell.util
|
||||
|
||||
|
||||
@@ -22,9 +22,11 @@ import uuid
|
||||
import hashlib
|
||||
import zipfile
|
||||
import tempfile
|
||||
import traceback
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.ifcopenshell_wrapper
|
||||
import blenderbim
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
from pathlib import Path
|
||||
@@ -37,19 +39,19 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
|
||||
|
||||
class IfcStore:
|
||||
path: str = ""
|
||||
file: ifcopenshell.file = None
|
||||
schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None
|
||||
cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None
|
||||
cache_path: str = None
|
||||
file: Optional[ifcopenshell.file] = None
|
||||
schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None
|
||||
cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None
|
||||
cache_path: Optional[str] = None
|
||||
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
|
||||
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
|
||||
edited_objs: Set[bpy.types.Object] = set()
|
||||
pset_template_path: str = ""
|
||||
pset_template_file: ifcopenshell.file = None
|
||||
pset_template_file: Optional[ifcopenshell.file] = None
|
||||
classification_path: str = ""
|
||||
classification_file: ifcopenshell.file = None
|
||||
classification_file: Optional[ifcopenshell.file] = None
|
||||
library_path: str = ""
|
||||
library_file: ifcopenshell.file = None
|
||||
library_file: Optional[ifcopenshell.file] = None
|
||||
current_transaction = ""
|
||||
last_transaction = ""
|
||||
history = []
|
||||
@@ -329,6 +331,7 @@ class IfcStore:
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False):
|
||||
blenderbim.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)
|
||||
|
||||
@@ -343,10 +346,14 @@ class IfcStore:
|
||||
else:
|
||||
operator.transaction_key = IfcStore.current_transaction
|
||||
|
||||
if is_invoke:
|
||||
result = getattr(operator, "_invoke")(context, None)
|
||||
else:
|
||||
result = getattr(operator, "_execute")(context)
|
||||
try:
|
||||
if is_invoke:
|
||||
result = getattr(operator, "_invoke")(context, None)
|
||||
else:
|
||||
result = getattr(operator, "_execute")(context)
|
||||
except:
|
||||
blenderbim.last_error = traceback.format_exc()
|
||||
raise
|
||||
|
||||
if is_top_level_operator:
|
||||
if tool.Ifc.get():
|
||||
|
||||
@@ -34,6 +34,7 @@ import ifcopenshell.util.element
|
||||
import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
|
||||
from itertools import chain, accumulate
|
||||
@@ -199,7 +200,7 @@ class MaterialCreator:
|
||||
|
||||
|
||||
class IfcImporter:
|
||||
def __init__(self, ifc_import_settings):
|
||||
def __init__(self, ifc_import_settings: IfcImportSettings):
|
||||
self.ifc_import_settings = ifc_import_settings
|
||||
self.diff = None
|
||||
self.file: ifcopenshell.file = None
|
||||
@@ -298,6 +299,7 @@ class IfcImporter:
|
||||
if self.ifc_import_settings.should_setup_viewport_camera:
|
||||
self.setup_viewport_camera()
|
||||
self.setup_arrays()
|
||||
self.profile_code("Setup arrays")
|
||||
self.update_progress(100)
|
||||
bpy.context.window_manager.progress_end()
|
||||
|
||||
@@ -430,7 +432,7 @@ class IfcImporter:
|
||||
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
|
||||
self.annotations -= drawing_annotations
|
||||
|
||||
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
|
||||
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
|
||||
if self.ifc_import_settings.is_coordinating:
|
||||
self.elements = [e for e in self.elements if e.Representation]
|
||||
|
||||
@@ -601,6 +603,9 @@ class IfcImporter:
|
||||
return products
|
||||
|
||||
def predict_dense_mesh(self):
|
||||
if self.ifc_import_settings.should_use_native_meshes:
|
||||
return
|
||||
|
||||
threshold = 10000 # Just from experience.
|
||||
|
||||
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
|
||||
@@ -725,7 +730,7 @@ class IfcImporter:
|
||||
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
|
||||
return True
|
||||
|
||||
def apply_blender_offset_to_matrix_world(self, obj, matrix):
|
||||
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset:
|
||||
if obj.data and obj.data.get("has_cartesian_point_offset", None):
|
||||
@@ -857,7 +862,10 @@ class IfcImporter:
|
||||
print("Done creating geometry")
|
||||
|
||||
def create_spatial_elements(self) -> None:
|
||||
self.create_generic_elements(self.spatial_elements, unselectable=True)
|
||||
if bpy.context.preferences.addons["blenderbim"].preferences.spatial_elements_unselectable:
|
||||
self.create_generic_elements(self.spatial_elements, unselectable=True)
|
||||
else:
|
||||
self.create_generic_elements(self.spatial_elements, unselectable=False)
|
||||
|
||||
def create_elements(self) -> None:
|
||||
self.create_generic_elements(self.elements)
|
||||
@@ -949,7 +957,9 @@ class IfcImporter:
|
||||
self.create_product(element, mesh=mesh)
|
||||
|
||||
def create_products(
|
||||
self, products, settings: Optional[ifcopenshell.geom.main.settings] = None
|
||||
self,
|
||||
products: set[ifcopenshell.entity_instance],
|
||||
settings: Optional[ifcopenshell.geom.main.settings] = None,
|
||||
) -> set[ifcopenshell.entity_instance]:
|
||||
results = set()
|
||||
if not products:
|
||||
@@ -1164,6 +1174,9 @@ class IfcImporter:
|
||||
|
||||
return obj
|
||||
|
||||
def load_existing_meshes(self) -> None:
|
||||
self.meshes.update({m.name: m for m in bpy.data.meshes})
|
||||
|
||||
def get_representation_item_material_name(self, item):
|
||||
if not item.StyledByItem:
|
||||
return
|
||||
@@ -1485,7 +1498,11 @@ class IfcImporter:
|
||||
# Occurs when reloading a project
|
||||
pass
|
||||
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
|
||||
project_collection.children[self.type_collection.name].hide_viewport = True
|
||||
types_collection = project_collection.children[self.type_collection.name]
|
||||
types_collection.hide_viewport = False
|
||||
for obj in types_collection.collection.objects: #turn off all objects inside Types collection.
|
||||
obj.hide_set(True)
|
||||
|
||||
|
||||
def clean_mesh(self):
|
||||
obj = None
|
||||
@@ -1620,30 +1637,34 @@ class IfcImporter:
|
||||
if self.ifc_import_settings.has_filter:
|
||||
rel_aggregates = set()
|
||||
for element in self.elements:
|
||||
if element.IsDecomposedBy:
|
||||
rel_aggregates.add(element.IsDecomposedBy[0])
|
||||
elif element.Decomposes:
|
||||
rel_aggregates.add(element.Decomposes[0])
|
||||
elif getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
|
||||
if [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")]:
|
||||
rel_aggregates.add(element.IsNestedBy[0])
|
||||
elif getattr(element, "Nests", []):
|
||||
rel_aggregates.add(element.Nests[0])
|
||||
if decomposed_by := element.IsDecomposedBy:
|
||||
rel_aggregates.add(decomposed_by[0])
|
||||
elif decomposes := element.Decomposes:
|
||||
rel_aggregates.add(decomposes[0])
|
||||
elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
|
||||
if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None):
|
||||
rel_aggregates.add(nested_by[0])
|
||||
elif nests := getattr(element, "Nests", []):
|
||||
rel_aggregates.add(nests[0])
|
||||
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
|
||||
rel_aggregates.add(element.AdheresToElement[0])
|
||||
else:
|
||||
rel_aggregates = [
|
||||
r
|
||||
for r in self.file.by_type("IfcRelAggregates")
|
||||
if r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType")
|
||||
if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType")
|
||||
] + [
|
||||
r
|
||||
for r in self.file.by_type("IfcRelNests")
|
||||
if (
|
||||
r.RelatingObject.is_a("IfcElement")
|
||||
or r.RelatingObject.is_a("IfcElementType")
|
||||
or (r.RelatingObject.is_a("IfcPositioningElement") and not r.RelatingObject.is_a("IfcGrid"))
|
||||
(relating_obj := r.RelatingObject).is_a("IfcElement")
|
||||
or relating_obj.is_a("IfcElementType")
|
||||
or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid"))
|
||||
)
|
||||
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
|
||||
]
|
||||
if self.file.schema == "IFC4X3":
|
||||
rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")]
|
||||
|
||||
if len(rel_aggregates) > 10000:
|
||||
# More than 10,000 collections makes Blender unhappy
|
||||
@@ -1653,7 +1674,9 @@ class IfcImporter:
|
||||
|
||||
aggregates: dict[str, dict] = {}
|
||||
for rel_aggregate in rel_aggregates:
|
||||
element: ifcopenshell.entity_instance = rel_aggregate.RelatingObject
|
||||
element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr(
|
||||
rel_aggregate, "RelatingElement"
|
||||
)
|
||||
collection = bpy.data.collections.new(tool.Loader.get_name(element))
|
||||
aggregates[element.GlobalId] = {"element": element, "collection": collection}
|
||||
self.collections[element.GlobalId] = collection
|
||||
@@ -1754,6 +1777,9 @@ class IfcImporter:
|
||||
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
return self.collections[nest.GlobalId].objects.link(obj)
|
||||
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
|
||||
adherend = element.AdheresToElement[0].RelatingElement
|
||||
return self.collections[adherend.GlobalId].objects.link(obj)
|
||||
|
||||
return self.place_object_in_spatial_decomposition_collection(element, obj)
|
||||
|
||||
@@ -1803,7 +1829,7 @@ class IfcImporter:
|
||||
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
|
||||
return rel.RelatingGroup
|
||||
|
||||
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.array:
|
||||
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.ndarray:
|
||||
if isinstance(element, ifcopenshell.sqlite_entity):
|
||||
result = self.geometry_cache["shapes"][element.id()]["matrix"]
|
||||
else:
|
||||
@@ -1925,7 +1951,9 @@ class IfcImporter:
|
||||
|
||||
# See bug 3546
|
||||
# ios_edges holds true edges that aren't triangulated.
|
||||
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
|
||||
#
|
||||
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
|
||||
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
|
||||
|
||||
mesh.vertices.add(num_vertices)
|
||||
mesh.vertices.foreach_set("co", verts)
|
||||
@@ -1952,14 +1980,14 @@ class IfcImporter:
|
||||
|
||||
print(traceback.format_exc())
|
||||
|
||||
def a2p(self, o, z, x):
|
||||
def a2p(self, o: mathutils.Vector, z: mathutils.Vector, x: mathutils.Vector) -> mathutils.Matrix:
|
||||
y = z.cross(x)
|
||||
r = mathutils.Matrix((x, y, z, o))
|
||||
r.resize_4x4()
|
||||
r.transpose()
|
||||
return r
|
||||
|
||||
def get_axis2placement(self, plc):
|
||||
def get_axis2placement(self, plc: ifcopenshell.entity_instance) -> mathutils.Matrix:
|
||||
if plc.is_a("IfcAxis2Placement3D"):
|
||||
z = mathutils.Vector(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
|
||||
x = mathutils.Vector(plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
|
||||
@@ -1979,7 +2007,7 @@ class IfcImporter:
|
||||
o = plc.LocalOrigin.Coordinates
|
||||
return self.a2p(o, z, x)
|
||||
|
||||
def get_local_placement(self, plc):
|
||||
def get_local_placement(self, plc: Optional[ifcopenshell.entity_instance] = None) -> mathutils.Matrix:
|
||||
if plc is None:
|
||||
return mathutils.Matrix()
|
||||
if plc.PlacementRelTo is None:
|
||||
@@ -1994,11 +2022,11 @@ class IfcImporter:
|
||||
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
|
||||
break
|
||||
|
||||
def link_element(self, element, obj):
|
||||
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
|
||||
self.added_data[element.id()] = obj
|
||||
tool.Ifc.link(element, obj)
|
||||
|
||||
def set_matrix_world(self, obj, matrix_world):
|
||||
def set_matrix_world(self, obj: bpy.types.Object, matrix_world: mathutils.Matrix) -> None:
|
||||
obj.matrix_world = matrix_world
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
@@ -2021,7 +2049,7 @@ class IfcImporter:
|
||||
|
||||
class IfcImportSettings:
|
||||
def __init__(self):
|
||||
self.logger = None
|
||||
self.logger: logging.Logger = None
|
||||
self.input_file = None
|
||||
self.diff_file = None
|
||||
self.should_use_cpu_multiprocessing = True
|
||||
|
||||
@@ -96,12 +96,13 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
|
||||
)
|
||||
|
||||
# Removes Pset related to Linked Aggregates
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate")
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
|
||||
if not element.is_a('IfcElementAssembly'):
|
||||
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
|
||||
|
||||
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
|
||||
"""Enable editing aggregation relationship"""
|
||||
|
||||
@@ -132,6 +133,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
|
||||
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
@@ -139,13 +141,15 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def draw(self, context):
|
||||
row = self.layout
|
||||
row.prop(self, "ifc_class")
|
||||
row = self.layout
|
||||
row.prop(self, "aggregate_name")
|
||||
|
||||
def _execute(self, context):
|
||||
try:
|
||||
ifc_class = tool.Ifc.schema().declaration_by_name(self.ifc_class).name()
|
||||
except:
|
||||
return
|
||||
aggregate = self.create_aggregate(context, ifc_class)
|
||||
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -173,8 +177,8 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
|
||||
|
||||
def create_aggregate(self, context, ifc_class):
|
||||
aggregate = bpy.data.objects.new("Assembly", None)
|
||||
def create_aggregate(self, context, ifc_class, aggregate_name):
|
||||
aggregate = bpy.data.objects.new(aggregate_name, None)
|
||||
aggregate.location = context.scene.cursor.location
|
||||
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
|
||||
return aggregate
|
||||
@@ -190,12 +194,17 @@ class BIM_OT_select_parts(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj))
|
||||
parts_objs = set(tool.Ifc.get_object(part) for part in parts)
|
||||
selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs)
|
||||
for selectable_part_obj in selectable_parts_objs:
|
||||
selectable_part_obj.select_set(True)
|
||||
# obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
|
||||
for obj in context.selected_objects:
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj))
|
||||
if parts:
|
||||
parts_objs = set(tool.Ifc.get_object(part) for part in parts)
|
||||
selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs)
|
||||
for selectable_part_obj in selectable_parts_objs:
|
||||
selectable_part_obj.select_set(True)
|
||||
else:
|
||||
obj.select_set(False)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -217,18 +226,43 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
|
||||
# obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
# aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
|
||||
# aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
|
||||
all_parts = []
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
all_parts.append(aggregate)
|
||||
obj.select_set(False)
|
||||
else:
|
||||
pass
|
||||
if not element:
|
||||
obj.select_set(False)
|
||||
|
||||
if self.select_parts:
|
||||
bpy.ops.bim.select_parts(obj=aggregate_obj.name)
|
||||
if aggregate_obj in context.selectable_objects:
|
||||
tool.Blender.set_objects_selection(
|
||||
context,
|
||||
aggregate_obj,
|
||||
[aggregate_obj],
|
||||
clear_previous_selection=not self.select_parts,
|
||||
)
|
||||
all_objs = []
|
||||
for part in all_parts:
|
||||
if part.IsDecomposedBy:
|
||||
for subpart in part.IsDecomposedBy[0].RelatedObjects:
|
||||
all_parts.append(subpart)
|
||||
all_objs.append(part)
|
||||
|
||||
for element in all_objs:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
|
||||
else:
|
||||
for aggregate_element in all_parts:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate_element)
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -283,7 +317,7 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], product=aggregate)
|
||||
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[aggregate])
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -292,28 +326,51 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.select_linked_aggregates"
|
||||
bl_label = "Select linked aggregates"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
select_parts: bpy.props.BoolProperty(default=False)
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.select_parts:
|
||||
return "Select all aggregates, subaggregates and all their parts"
|
||||
else:
|
||||
return "Select all aggregates"
|
||||
|
||||
def _execute(self, context):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if not aggregate:
|
||||
return []
|
||||
if not element:
|
||||
return []
|
||||
|
||||
linked_aggregate_group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
|
||||
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if not aggregate:
|
||||
continue
|
||||
if not element:
|
||||
continue
|
||||
|
||||
for rel in linked_aggregate_group[0].IsGroupedBy or []:
|
||||
for element in rel.RelatedObjects:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj.select_set(True)
|
||||
linked_aggregate_group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
|
||||
group_rel = linked_aggregate_group[0].IsGroupedBy or []
|
||||
for group_link in group_rel:
|
||||
parts = list(group_link.RelatedObjects)
|
||||
if self.select_parts:
|
||||
parts_objs = []
|
||||
for part in parts:
|
||||
if part.IsDecomposedBy:
|
||||
for subpart in part.IsDecomposedBy[0].RelatedObjects:
|
||||
parts.append(subpart)
|
||||
parts_objs.append(part)
|
||||
|
||||
for element in parts_objs:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
else:
|
||||
for element in parts:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj.select_set(True)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -20,6 +20,7 @@ from bpy.types import Panel
|
||||
from blenderbim.bim.module.aggregate.data import AggregateData
|
||||
from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class BIM_PT_aggregate(Panel):
|
||||
@@ -131,19 +132,29 @@ class BIM_PT_linked_aggregate(Panel):
|
||||
if not AggregateData.is_loaded:
|
||||
AggregateData.load()
|
||||
|
||||
props = context.active_object.BIMObjectAggregateProperties
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMObjectAggregateProperties
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Advanced Users Only", icon="ERROR")
|
||||
row = layout.row(align=True)
|
||||
|
||||
if type(AggregateData.data['total_linked_aggregate']) is int:
|
||||
if AggregateData.data['total_linked_aggregate'] > 0:
|
||||
row.label(text=f"{AggregateData.data['total_linked_aggregate']} Linked Aggregates")
|
||||
row.operator("bim.select_linked_aggregates", text="", icon="RESTRICT_SELECT_OFF")
|
||||
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
|
||||
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
|
||||
|
||||
|
||||
if element.Decomposes:
|
||||
Number_Linked_Aggregates = AggregateData.data['total_linked_aggregate']
|
||||
if not Number_Linked_Aggregates:
|
||||
row.label(text="Not a Linked Aggregate")
|
||||
else:
|
||||
row.label(text=f"{Number_Linked_Aggregates} Linked Aggregates")
|
||||
op = row.operator("bim.object_duplicate_move_linked_aggregate", text="", icon="DUPLICATE")
|
||||
if type(Number_Linked_Aggregates) is int:
|
||||
if Number_Linked_Aggregates > 0:
|
||||
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
|
||||
op.select_parts = False
|
||||
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
|
||||
op.select_parts = True
|
||||
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
|
||||
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
|
||||
else:
|
||||
row.label(text="No Linked Aggregates")
|
||||
row.label(text="Not an Aggregate")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.guid
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
@@ -47,10 +48,18 @@ class EnableEditingAttributes(bpy.types.Operator):
|
||||
obj = bpy.data.objects.get(self.obj)
|
||||
elif self.obj_type == "Material":
|
||||
obj = bpy.data.materials.get(self.obj)
|
||||
oprops = obj.BIMObjectProperties
|
||||
props = obj.BIMAttributeProperties
|
||||
props.attributes.clear()
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
has_inherited_predefined_type = False
|
||||
if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)):
|
||||
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
|
||||
has_inherited_predefined_type = ifcopenshell.util.element.get_predefined_type(element_type) not in (
|
||||
"NOTDEFINED",
|
||||
None,
|
||||
)
|
||||
|
||||
def callback(name, prop, data):
|
||||
if name in ("RefLatitude", "RefLongitude"):
|
||||
new = props.attributes.add()
|
||||
@@ -62,10 +71,11 @@ class EnableEditingAttributes(bpy.types.Operator):
|
||||
new.string_value = "" if new.is_null else json.dumps(data[name])
|
||||
blenderbim.bim.helper.add_attribute_description(new)
|
||||
new.description += " The degrees, minutes and seconds should follow this format : [12,34,56]"
|
||||
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
|
||||
props.attributes.remove(len(props.attributes) - 1)
|
||||
return True
|
||||
|
||||
blenderbim.bim.helper.import_attributes2(
|
||||
tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback
|
||||
)
|
||||
blenderbim.bim.helper.import_attributes2(element, props.attributes, callback=callback)
|
||||
props.is_editing_attributes = True
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -120,20 +130,46 @@ class EditAttributes(bpy.types.Operator, Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GenerateGlobalId(bpy.types.Operator):
|
||||
class GenerateGlobalId(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.generate_global_id"
|
||||
bl_label = "Regenerate GlobalId"
|
||||
bl_description = "Regenerate GlobalId\n\nSHIFT+CLICK to regenerate GlobalIds for all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
|
||||
if index >= 0:
|
||||
global_id = context.active_object.BIMAttributeProperties.attributes[index]
|
||||
use_selected: bpy.props.BoolProperty(name="Use All Selected Objects", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# using all selected objects on shift+click
|
||||
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.use_selected = True
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
if self.use_selected:
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcRoot"):
|
||||
continue
|
||||
element.GlobalId = ifcopenshell.guid.new()
|
||||
|
||||
obj = context.active_object
|
||||
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
|
||||
return {"FINISHED"}
|
||||
|
||||
props = obj.BIMAttributeProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if not element.is_a("IfcRoot"):
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.use_selected and obj in context.selected_objects:
|
||||
# guid value was already regenerated, just update the ui prop
|
||||
guid_value = element.GlobalId
|
||||
else:
|
||||
global_id = context.active_object.BIMAttributeProperties.attributes.add()
|
||||
global_id.name = "GlobalId"
|
||||
global_id.data_type = "string"
|
||||
global_id.string_value = ifcopenshell.guid.new()
|
||||
guid_value = ifcopenshell.guid.new()
|
||||
|
||||
props.attributes["GlobalId"].string_value = guid_value
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import bmesh
|
||||
import logging
|
||||
import shapely
|
||||
import shapely.ops
|
||||
import mathutils
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
|
||||
@@ -24,10 +24,10 @@ from math import pi
|
||||
|
||||
class BIMCadProperties(PropertyGroup):
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
||||
x: bpy.props.FloatProperty(name="X", default=0.2)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
|
||||
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
|
||||
gable_roof_edge_angle: bpy.props.FloatProperty(
|
||||
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
|
||||
@@ -239,8 +240,9 @@ 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 self.is_profile():
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
|
||||
else:
|
||||
bpy.ops.bim.cad_arc_from_2_points()
|
||||
|
||||
@@ -248,13 +250,15 @@ 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 self.is_profile():
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
|
||||
else:
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
|
||||
|
||||
def hotkey_S_O(self):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
@@ -270,7 +274,8 @@ class CadHotkey(bpy.types.Operator):
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
|
||||
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)
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
|
||||
@@ -17,138 +17,36 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.date
|
||||
import ifcopenshell.util.classification
|
||||
import json
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def refresh():
|
||||
ClassificationsData.is_loaded = False
|
||||
ClassificationReferencesData.is_loaded = False
|
||||
MaterialClassificationsData.is_loaded = False
|
||||
CostClassificationsData.is_loaded = False
|
||||
ClashData.is_loaded = False
|
||||
|
||||
|
||||
class ClassificationsData:
|
||||
class ClashData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["has_classification_file"] = cls.has_classification_file()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["available_classifications"] = cls.available_classifications()
|
||||
cls.data = {}
|
||||
cls.data["saved_searches"] = cls.saved_searches()
|
||||
|
||||
@classmethod
|
||||
def has_classification_file(cls):
|
||||
return bool(IfcStore.classification_file)
|
||||
|
||||
@classmethod
|
||||
def classifications(cls):
|
||||
results = []
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
data = element.get_info()
|
||||
if tool.Ifc.get().schema == "IFC2X3" and element.EditionDate:
|
||||
data["EditionDate"] = ifcopenshell.util.date.ifc2datetime(data["EditionDate"])
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def available_classifications(cls):
|
||||
if not IfcStore.classification_file:
|
||||
def saved_searches(cls):
|
||||
if not tool.Ifc.get():
|
||||
return []
|
||||
return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")]
|
||||
|
||||
|
||||
class ReferencesData:
|
||||
@classmethod
|
||||
def active_classification_library(cls):
|
||||
if not IfcStore.classification_file or not IfcStore.classification_file.by_type("IfcClassification"):
|
||||
return False
|
||||
props = bpy.context.scene.BIMClassificationProperties
|
||||
name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
|
||||
if name in [e.Name for e in tool.Ifc.get().by_type("IfcClassification")]:
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def classifications(cls):
|
||||
return [(str(e.id()), e.Name, "") for e in tool.Ifc.get().by_type("IfcClassification")]
|
||||
|
||||
|
||||
class ClassificationReferencesData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["active_classification_library"] = cls.active_classification_library()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["object_type"] = "OBJECT"
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
groups = tool.Ifc.get().by_type("IfcGroup")
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
class MaterialClassificationsData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["active_classification_library"] = cls.active_classification_library()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["object_type"] = "MATERIAL"
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
class CostClassificationsData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["active_classification_library"] = cls.active_classification_library()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["object_type"] = "COST"
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get().by_id(
|
||||
bpy.context.scene.BIMCostProperties.cost_items[
|
||||
bpy.context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
for group in groups:
|
||||
try:
|
||||
data = json.loads(group.Description)
|
||||
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
|
||||
results.append(group)
|
||||
except:
|
||||
pass
|
||||
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
|
||||
|
||||
@@ -16,31 +16,37 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blf
|
||||
import gpu
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
|
||||
|
||||
class ClashDecorator:
|
||||
installed = None
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.installed:
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.installed = None
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
@@ -48,7 +54,27 @@ class ClashDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def __call__(self, context):
|
||||
def draw_text(self, context):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
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)
|
||||
|
||||
font_id = 0
|
||||
blf.size(font_id, 12)
|
||||
coords_2d = location_3d_to_region_2d(context.region, context.region_data, p)
|
||||
color = self.addon_prefs.decorations_colour
|
||||
blf.color(font_id, *color)
|
||||
if coords_2d:
|
||||
w, h = blf.dimensions(font_id, text)
|
||||
coords_2d -= Vector((w * .5, 0))
|
||||
blf.position(font_id, coords_2d[0], coords_2d[1], 0)
|
||||
blf.draw(font_id, text) # Set your text here
|
||||
|
||||
def draw_geometry(self, context):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
|
||||
@@ -69,6 +69,7 @@ class ImportClashSets(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
tool.Clash.load_clash_sets(self.filepath)
|
||||
context.scene.BIMClashProperties.clash_sets.clear()
|
||||
for clash_set in tool.Clash.get_clash_sets():
|
||||
new = context.scene.BIMClashProperties.clash_sets.add()
|
||||
new.name = clash_set["name"]
|
||||
@@ -85,14 +86,14 @@ class ImportClashSets(bpy.types.Operator):
|
||||
new_source = new.a.add()
|
||||
new_source.name = clash_source["file"]
|
||||
if "selector" in clash_source:
|
||||
new_source.selector = clash_source["selector"]
|
||||
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
|
||||
new_source.mode = clash_source["mode"]
|
||||
if "b" in clash_set and clash_set["b"]:
|
||||
for clash_source in clash_set["b"]:
|
||||
new_source = new.b.add()
|
||||
new_source.name = clash_source["file"]
|
||||
if "selector" in clash_source:
|
||||
new_source.selector = clash_source["selector"]
|
||||
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
|
||||
new_source.mode = clash_source["mode"]
|
||||
tool.Clash.import_active_clashes()
|
||||
return {"FINISHED"}
|
||||
@@ -208,8 +209,11 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension != ".json":
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
|
||||
if extension != ".bcf":
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
# TODO Temporarily until BCF support comes back
|
||||
# if extension != ".json":
|
||||
# self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
@@ -220,6 +224,9 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
self.props = context.scene.BIMClashProperties
|
||||
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension != ".bcf":
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
# TODO Temporarily until BCF support comes back
|
||||
if extension != ".json":
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
|
||||
|
||||
@@ -276,14 +283,7 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
|
||||
if extension == ".json":
|
||||
tool.Clash.load_clash_sets(self.filepath)
|
||||
result = tool.Clash.get_clash_set(self.props.active_clash_set.name)
|
||||
for clash in result["clashes"].values():
|
||||
blender_clash = self.props.active_clash_set.clashes.add()
|
||||
blender_clash.a_global_id = clash["a_global_id"]
|
||||
blender_clash.b_global_id = clash["b_global_id"]
|
||||
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
|
||||
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
|
||||
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
|
||||
tool.Clash.import_active_clashes()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -379,6 +379,7 @@ class SelectClash(bpy.types.Operator):
|
||||
tool.Clash.look_at(target, target + Vector((5, 5, 5)))
|
||||
self.props.p1 = clash["p1"]
|
||||
self.props.p2 = clash["p2"]
|
||||
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.prop import StrProperty, Attribute, BIMFilterGroup
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -33,11 +33,12 @@ from bpy.props import (
|
||||
|
||||
class ClashSource(PropertyGroup):
|
||||
name: StringProperty(name="File")
|
||||
selector: StringProperty(name="Selector")
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
mode: EnumProperty(
|
||||
items=[
|
||||
("i", "Include", "Only the selected objects are included for clashing"),
|
||||
("e", "Exclude", "All objects except the selected objects are included for clashing"),
|
||||
("a", "All Elements", "All elements will be used for clashing"),
|
||||
("i", "Include", "Only the selected elements are included for clashing"),
|
||||
("e", "Exclude", "All elements except the selected elements are included for clashing"),
|
||||
],
|
||||
name="Mode",
|
||||
)
|
||||
@@ -97,6 +98,7 @@ class BIMClashProperties(PropertyGroup):
|
||||
)
|
||||
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
|
||||
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
|
||||
active_clash_text: StringProperty(name="Active Clash Text")
|
||||
|
||||
@property
|
||||
def active_clash_set(self):
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim.helper
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.module.clash.data import ClashData
|
||||
|
||||
|
||||
class BIM_PT_ifcclash(Panel):
|
||||
@@ -30,6 +32,9 @@ class BIM_PT_ifcclash(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_clash_detection"
|
||||
|
||||
def draw(self, context):
|
||||
if not ClashData.is_loaded:
|
||||
ClashData.load()
|
||||
|
||||
layout = self.layout
|
||||
|
||||
scene = context.scene
|
||||
@@ -78,6 +83,7 @@ class BIM_PT_ifcclash(Panel):
|
||||
for index, source in enumerate(clash_set.a):
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "name", text="")
|
||||
row.prop(source, "mode", text="")
|
||||
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
|
||||
op.index = index
|
||||
op.group = "a"
|
||||
@@ -85,9 +91,10 @@ class BIM_PT_ifcclash(Panel):
|
||||
op.index = index
|
||||
op.group = "a"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "mode", text="")
|
||||
row.prop(source, "selector", text="")
|
||||
if source.mode != "a":
|
||||
blenderbim.bim.helper.draw_filter(
|
||||
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
|
||||
)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
|
||||
@@ -96,6 +103,7 @@ class BIM_PT_ifcclash(Panel):
|
||||
for index, source in enumerate(clash_set.b):
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "name", text="")
|
||||
row.prop(source, "mode", text="")
|
||||
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
|
||||
op.index = index
|
||||
op.group = "b"
|
||||
@@ -103,9 +111,10 @@ class BIM_PT_ifcclash(Panel):
|
||||
op.index = index
|
||||
op.group = "b"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "mode", text="")
|
||||
row.prop(source, "selector", text="")
|
||||
if source.mode != "a":
|
||||
blenderbim.bim.helper.draw_filter(
|
||||
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
|
||||
)
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "should_create_clash_snapshots")
|
||||
|
||||
@@ -20,6 +20,8 @@ import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.classification
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.helper
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
@@ -87,7 +89,7 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
reference = ifcopenshell.api.run(
|
||||
"classification.add_reference",
|
||||
tool.Ifc.get(),
|
||||
product=product,
|
||||
products=[product],
|
||||
classification=classification,
|
||||
identification="X",
|
||||
name="Unnamed",
|
||||
@@ -207,6 +209,10 @@ class DisableEditingClassification(bpy.types.Operator):
|
||||
class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_classification"
|
||||
bl_label = "Remove Classification"
|
||||
bl_description = (
|
||||
"The classification and all of its relationships, children references, "
|
||||
"and relationships between objects and child references will be completely removed from a project"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
classification: bpy.props.IntProperty()
|
||||
|
||||
@@ -292,6 +298,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
active_reference = tool.Ifc.get().by_id(self.reference)
|
||||
identification = active_reference[1]
|
||||
|
||||
elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {}
|
||||
for obj in objects:
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
@@ -300,12 +307,16 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if (identification and reference[1] == identification) or (
|
||||
not identification and reference == active_reference
|
||||
):
|
||||
ifcopenshell.api.run(
|
||||
"classification.remove_reference",
|
||||
tool.Ifc.get(),
|
||||
reference=reference,
|
||||
product=element,
|
||||
)
|
||||
elements_by_references.setdefault(reference, []).append(element)
|
||||
|
||||
if elements_by_references:
|
||||
for reference, products in elements_by_references.items():
|
||||
ifcopenshell.api.run(
|
||||
"classification.remove_reference",
|
||||
tool.Ifc.get(),
|
||||
reference=reference,
|
||||
products=products,
|
||||
)
|
||||
|
||||
|
||||
class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -357,15 +368,18 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
classification = element
|
||||
break
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
|
||||
if not ifc_definition_id:
|
||||
continue
|
||||
ifc_file = tool.Ifc.get()
|
||||
products = [
|
||||
ifc_file.by_id(ifc_definition_id)
|
||||
for obj in objects
|
||||
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
|
||||
]
|
||||
if products:
|
||||
ifcopenshell.api.run(
|
||||
"classification.add_reference",
|
||||
tool.Ifc.get(),
|
||||
reference=IfcStore.classification_file.by_id(self.reference),
|
||||
product=tool.Ifc.get().by_id(ifc_definition_id),
|
||||
products=products,
|
||||
classification=classification,
|
||||
)
|
||||
|
||||
@@ -413,7 +427,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
reference = ifcopenshell.api.run(
|
||||
"classification.add_reference",
|
||||
tool.Ifc.get(),
|
||||
product=element,
|
||||
products=[element],
|
||||
classification=classification,
|
||||
identification=bsdd_classification.reference_code,
|
||||
name=bsdd_classification.name,
|
||||
|
||||
@@ -114,7 +114,7 @@ class EditObjective(bpy.types.Operator):
|
||||
ifcopenshell.api.run(
|
||||
"constraint.edit_objective",
|
||||
self.file,
|
||||
**{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes}
|
||||
**{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes},
|
||||
)
|
||||
bpy.ops.bim.load_objectives()
|
||||
return {"FINISHED"}
|
||||
@@ -179,19 +179,17 @@ class AssignConstraint(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
for obj in objs:
|
||||
obj_id = obj.BIMObjectProperties.ifc_definition_id
|
||||
if not obj_id:
|
||||
continue
|
||||
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
|
||||
if products:
|
||||
ifcopenshell.api.run(
|
||||
"constraint.assign_constraint",
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(obj_id),
|
||||
"products": products,
|
||||
"constraint": self.file.by_id(self.constraint),
|
||||
}
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -207,18 +205,16 @@ class UnassignConstraint(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
for obj in objs:
|
||||
obj_id = obj.BIMObjectProperties.ifc_definition_id
|
||||
if not obj_id:
|
||||
continue
|
||||
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
|
||||
if products:
|
||||
ifcopenshell.api.run(
|
||||
"constraint.unassign_constraint",
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(obj_id),
|
||||
"products": products,
|
||||
"constraint": self.file.by_id(self.constraint),
|
||||
}
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -53,6 +53,10 @@ class AddContext(bpy.types.Operator, Operator):
|
||||
class RemoveContext(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.remove_context"
|
||||
bl_label = "Remove Context"
|
||||
bl_description = (
|
||||
"Remove representation context. Any representation geometry that is assigned to the context is also removed. "
|
||||
"If a context is removed, then any subcontexts are also removed"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
context: bpy.props.IntProperty()
|
||||
|
||||
|
||||
@@ -107,59 +107,88 @@ class CoveringToolUI:
|
||||
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
|
||||
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
|
||||
|
||||
if (type_material_usage == "IfcMaterialLayerSet" and
|
||||
not bpy.context.selected_objects):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
if tool.Ifc.get_entity(collection_obj):
|
||||
if AuthoringData.data["predefined_type"] == "FLOORING":
|
||||
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
|
||||
elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
|
||||
else:
|
||||
op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
op.from_invoke = True
|
||||
if cls.props.relating_type_id.isnumeric():
|
||||
op.relating_type_id = int(cls.props.relating_type_id)
|
||||
|
||||
else:
|
||||
op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
op.from_invoke = True
|
||||
if cls.props.relating_type_id.isnumeric():
|
||||
op.relating_type_id = int(cls.props.relating_type_id)
|
||||
|
||||
elif (AuthoringData.data["predefined_type"] == "FLOORING" and
|
||||
type_material_usage == "IfcMaterialLayerSet" and
|
||||
element and
|
||||
bpy.context.selected_objects and
|
||||
element.is_a("IfcWall")):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
|
||||
|
||||
elif (AuthoringData.data["predefined_type"] == "CEILING" and
|
||||
type_material_usage == "IfcMaterialLayerSet" and
|
||||
element and
|
||||
bpy.context.selected_objects and
|
||||
element.is_a("IfcWall")):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
|
||||
|
||||
elif (element and
|
||||
bpy.context.selected_objects and
|
||||
element.is_a("IfcCovering") and
|
||||
# AuthoringData.data["predefined_type"] == "FLOORING" and
|
||||
AuthoringData.data["active_material_usage"] == "LAYER3"):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
op = row.operator("bim.regen_selected_covering_object")
|
||||
# if (type_material_usage == "IfcMaterialLayerSet" and
|
||||
# not bpy.context.selected_objects):
|
||||
# row = cls.layout.row(align=True)
|
||||
# row.label(text="", icon="EVENT_SHIFT")
|
||||
# row.label(text="", icon="EVENT_A")
|
||||
# if tool.Ifc.get_entity(collection_obj):
|
||||
# if AuthoringData.data["predefined_type"] == "FLOORING":
|
||||
# op = row.operator("bim.add_instance_flooring_covering_from_cursor")
|
||||
# elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
# op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
|
||||
# else:
|
||||
# op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
# op.from_invoke = True
|
||||
# if cls.props.relating_type_id.isnumeric():
|
||||
# op.relating_type_id = int(cls.props.relating_type_id)
|
||||
#
|
||||
# else:
|
||||
# op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
# op.from_invoke = True
|
||||
# if cls.props.relating_type_id.isnumeric():
|
||||
# op.relating_type_id = int(cls.props.relating_type_id)
|
||||
#
|
||||
# elif (AuthoringData.data["predefined_type"] == "FLOORING" and
|
||||
# type_material_usage == "IfcMaterialLayerSet" and
|
||||
# element and
|
||||
# bpy.context.selected_objects and
|
||||
# element.is_a("IfcWall")):
|
||||
# row = cls.layout.row(align=True)
|
||||
# row.label(text="", icon="EVENT_SHIFT")
|
||||
# row.label(text="", icon="EVENT_A")
|
||||
# op = row.operator("bim.add_instance_flooring_coverings_from_walls")
|
||||
#
|
||||
# elif (AuthoringData.data["predefined_type"] == "CEILING" and
|
||||
# type_material_usage == "IfcMaterialLayerSet" and
|
||||
# element and
|
||||
# bpy.context.selected_objects and
|
||||
# element.is_a("IfcWall")):
|
||||
# row = cls.layout.row(align=True)
|
||||
# row.label(text="", icon="EVENT_SHIFT")
|
||||
# row.label(text="", icon="EVENT_A")
|
||||
# op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
|
||||
#
|
||||
# elif (element and
|
||||
# bpy.context.selected_objects and
|
||||
# element.is_a("IfcCovering") and
|
||||
## AuthoringData.data["predefined_type"] == "FLOORING" and
|
||||
# AuthoringData.data["active_material_usage"] == "LAYER3"):
|
||||
# row = cls.layout.row(align=True)
|
||||
# row.label(text="", icon="EVENT_SHIFT")
|
||||
# row.label(text="", icon="EVENT_G")
|
||||
# op = row.operator("bim.regen_selected_covering_object")
|
||||
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
op = row.operator("bim.regen_selected_covering_object")
|
||||
|
||||
# elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
# row = cls.layout.row(align=True)
|
||||
@@ -174,14 +203,14 @@ class CoveringToolUI:
|
||||
# op.from_invoke = True
|
||||
# if cls.props.relating_type_id.isnumeric():
|
||||
# op.relating_type_id = int(cls.props.relating_type_id)
|
||||
else:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
op.from_invoke = True
|
||||
if cls.props.relating_type_id.isnumeric():
|
||||
op.relating_type_id = int(cls.props.relating_type_id)
|
||||
# else:
|
||||
# row = cls.layout.row(align=True)
|
||||
# row.label(text="", icon="EVENT_SHIFT")
|
||||
# row.label(text="", icon="EVENT_A")
|
||||
# op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
# op.from_invoke = True
|
||||
# if cls.props.relating_type_id.isnumeric():
|
||||
# op.relating_type_id = int(cls.props.relating_type_id)
|
||||
|
||||
@classmethod
|
||||
def draw_type_selection_interface(cls):
|
||||
|
||||
@@ -72,7 +72,7 @@ class ReorderCsvAttribute(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
old = context.scene.CsvProperties.csv_attributes[self.old_index]
|
||||
new = context.scene.CsvProperties.csv_attributes[self.new_index]
|
||||
props = ["name", "header", "sort", "group", "varies_value", "summary"]
|
||||
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
|
||||
for prop in props:
|
||||
value = getattr(new, prop)
|
||||
setattr(new, prop, getattr(old, prop))
|
||||
@@ -246,6 +246,7 @@ class ExportIfcCsv(bpy.types.Operator):
|
||||
if props.format != "csv" and props.should_generate_svg:
|
||||
schedule_creator = scheduler.Scheduler()
|
||||
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
|
||||
self.report({"INFO"}, f"Data is exported to {props.format.upper()}.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -285,6 +286,7 @@ class ImportIfcCsv(bpy.types.Operator):
|
||||
if not props.should_load_from_memory:
|
||||
ifc_file.write(props.csv_ifc_file)
|
||||
refresh_ui_data()
|
||||
self.report({"INFO"}, "Data is imported to IFC.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ classes = (
|
||||
operator.CopyDebugInformation,
|
||||
operator.CreateAllShapes,
|
||||
operator.CreateShapeFromStepId,
|
||||
operator.DebugActiveDrawing,
|
||||
operator.InspectFromObject,
|
||||
operator.InspectFromStepId,
|
||||
operator.OverrideDisplayType,
|
||||
@@ -34,7 +35,6 @@ classes = (
|
||||
operator.PrintUnusedElementStats,
|
||||
operator.ProfileImportIFC,
|
||||
operator.PurgeHdf5Cache,
|
||||
operator.PurgeIfcLinks,
|
||||
operator.PurgeUnusedElementsByClass,
|
||||
operator.RewindInspector,
|
||||
operator.SelectExpressFile,
|
||||
|
||||
@@ -20,18 +20,20 @@ import os
|
||||
import sys
|
||||
import bpy
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import addon_utils
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.debug as core
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.bim.import_ifc as import_ifc
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim import get_debug_info, format_debug_info
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
@@ -41,28 +43,7 @@ class CopyDebugInformation(bpy.types.Operator):
|
||||
bl_description = "Copies debugging information to your clipboard for use in bugreports"
|
||||
|
||||
def execute(self, context):
|
||||
version = ".".join(
|
||||
[
|
||||
str(x)
|
||||
for x in [
|
||||
addon.bl_info.get("version", (-1, -1, -1))
|
||||
for addon in addon_utils.modules()
|
||||
if addon.bl_info["name"] == "BlenderBIM"
|
||||
][0]
|
||||
]
|
||||
)
|
||||
info = {
|
||||
"os": platform.system(),
|
||||
"os_version": platform.version(),
|
||||
"python_version": platform.python_version(),
|
||||
"architecture": platform.architecture(),
|
||||
"machine": platform.machine(),
|
||||
"processor": platform.processor(),
|
||||
"blender_version": bpy.app.version_string,
|
||||
"blenderbim_version": version,
|
||||
"ifc": False,
|
||||
}
|
||||
|
||||
info = get_debug_info()
|
||||
if tool.Ifc.get():
|
||||
info.update(
|
||||
{
|
||||
@@ -73,16 +54,18 @@ class CopyDebugInformation(bpy.types.Operator):
|
||||
}
|
||||
)
|
||||
|
||||
# Format it in a readable way
|
||||
text = "\n".join(f"{k}: {v}" for k, v in info.items())
|
||||
text = format_debug_info(info)
|
||||
|
||||
print("-" * 80)
|
||||
print(text)
|
||||
print("-" * 80)
|
||||
|
||||
if platform.system() == "Windows":
|
||||
command = "echo | set /p nul=" + text.strip()
|
||||
command = "echo | set /p nul=" + text
|
||||
elif platform.system() == "Darwin": # for MacOS
|
||||
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy'
|
||||
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
|
||||
else: # Linux
|
||||
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
|
||||
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
|
||||
subprocess.run(command, shell=True, check=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -101,10 +84,11 @@ class PrintIfcFile(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class PurgeIfcLinks(bpy.types.Operator):
|
||||
bl_idname = "bim.purge_ifc_links"
|
||||
bl_label = "Purge IFC Links"
|
||||
bl_description = "Purge all definitions and references from the file.\nWarning : Cannot be undone."
|
||||
class ConvertToBlender(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_to_blender"
|
||||
bl_label = "Convert To Blender File"
|
||||
bl_description = "Removes all IFC data and revert to basic Blender objects.\nWarning : Cannot be undone."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
for obj in bpy.data.objects:
|
||||
@@ -121,26 +105,6 @@ class PurgeIfcLinks(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ConvertToBlender(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_to_blender"
|
||||
bl_label = "Convert To Blender File"
|
||||
bl_description = "Removes all IFC data, and converts the file to a simple Blender file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
for o in bpy.data.objects:
|
||||
if o.type in {"MESH", "EMPTY"}:
|
||||
o.BIMObjectProperties.ifc_definition_id = 0
|
||||
if o.data:
|
||||
o.data.BIMMeshProperties.ifc_definition_id = 0
|
||||
for m in bpy.data.materials:
|
||||
m.BIMMaterialProperties.ifc_style_id = False
|
||||
bpy.context.scene.BIMProperties.ifc_file = ""
|
||||
IfcStore.purge()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ValidateIfcFile(bpy.types.Operator):
|
||||
bl_idname = "bim.validate_ifc_file"
|
||||
bl_label = "Validate IFC File"
|
||||
@@ -660,3 +624,104 @@ class PipInstall(bpy.types.Operator):
|
||||
]
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DebugActiveDrawing(bpy.types.Operator):
|
||||
bl_idname = "bim.debug_active_drawing"
|
||||
bl_label = "Search Active Drawing For Failing Elements"
|
||||
bl_description = (
|
||||
"Will iterate over all visible drawing's objects, trying to narrow down the list of possible failing objects"
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
props = context.scene.DocProperties
|
||||
drawing_item = props.drawings[props.active_drawing_index]
|
||||
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
|
||||
|
||||
GREEN = "\033[92m"
|
||||
CYAN = "\033[96m"
|
||||
END = "\033[0m"
|
||||
ATTEMPS = 10
|
||||
|
||||
# run create drawing with sync for once
|
||||
# to make sure everything is actually in sync
|
||||
try:
|
||||
bpy.ops.bim.create_drawing(sync=True)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
self.report({"INFO"}, "No errors creating drawing, nothing to investigate.")
|
||||
return {"FINISHED"}
|
||||
|
||||
all_elements = [e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))]
|
||||
all_elements = set(all_elements)
|
||||
|
||||
original_exclude = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude")
|
||||
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
|
||||
|
||||
def drawing_fails_to_load(chunk_to_include: set) -> bool:
|
||||
current_elements = all_elements - chunk_to_include
|
||||
excluded_guids = ", ".join([e.GlobalId for e in current_elements if hasattr(e, "GlobalId")])
|
||||
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": f"{original_exclude}, {excluded_guids}"})
|
||||
|
||||
try:
|
||||
bpy.ops.bim.create_drawing(sync=False)
|
||||
result = False
|
||||
except Exception as e:
|
||||
# print(e)
|
||||
result = True
|
||||
|
||||
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": original_exclude})
|
||||
return result
|
||||
|
||||
def test_elements(elements: list[ifcopenshell.entity_instance], attempts: int = ATTEMPS) -> None:
|
||||
print(f"{CYAN}processing {len(elements)} elements{END}")
|
||||
if not elements:
|
||||
print(f"Empty list of elements, will stop...")
|
||||
return
|
||||
|
||||
n_elements = len(elements)
|
||||
middle = int(n_elements / 2)
|
||||
chunk1, chunk2 = elements[:middle], elements[middle:]
|
||||
test_chunk_1 = drawing_fails_to_load(set(chunk1))
|
||||
test_chunk_2 = drawing_fails_to_load(set(chunk2))
|
||||
|
||||
if (
|
||||
# both chunks do not fail anymore
|
||||
(not test_chunk_1 and not test_chunk_2)
|
||||
# or we have 1 element chunk that is still failing
|
||||
or (test_chunk_1 and not chunk2)
|
||||
or (test_chunk_2 and not chunk1)
|
||||
):
|
||||
if attempts == 0 or n_elements in (1, 2):
|
||||
print(f"{GREEN}Couldn't narrow it down any further.{END}")
|
||||
print(f"It's some of the {n_elements} elements:")
|
||||
print(elements)
|
||||
|
||||
print("Let's test excluding them...")
|
||||
for element in elements:
|
||||
test = drawing_fails_to_load(all_elements - {element})
|
||||
if test:
|
||||
print(f"{CYAN}Excluding element didn't fixed the drawing: {END}")
|
||||
print(element)
|
||||
else:
|
||||
print(f"{GREEN}Excluding element fixed the drawing: {END}")
|
||||
print(element)
|
||||
else:
|
||||
print(f"{CYAN}Will try to reshuffle elements and try again, attempt {ATTEMPS-attempts+1}/{ATTEMPS}")
|
||||
attempts -= 1
|
||||
random.shuffle(elements)
|
||||
test_elements(elements, attempts)
|
||||
return
|
||||
|
||||
# if chunk fails we need to investigate it further
|
||||
if test_chunk_1:
|
||||
test_elements(chunk1)
|
||||
|
||||
if test_chunk_2:
|
||||
test_elements(chunk2)
|
||||
|
||||
test_elements(list(all_elements))
|
||||
|
||||
self.report({"INFO"}, "See system console for the results")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -60,9 +60,6 @@ class BIM_PT_debug(Panel):
|
||||
row = layout.row()
|
||||
row.operator("bim.purge_hdf5_cache")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.purge_ifc_links")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.update_representation", text="Manually Save Representation")
|
||||
|
||||
@@ -75,6 +72,9 @@ class BIM_PT_debug(Panel):
|
||||
row = layout.row()
|
||||
row.operator("bim.profile_import_ifc")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.debug_active_drawing")
|
||||
|
||||
row = layout.split(factor=0.5, align=True)
|
||||
row.operator("bim.create_shape_from_step_id").should_include_curves = False
|
||||
row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True
|
||||
|
||||
@@ -38,6 +38,7 @@ class DiffData:
|
||||
cls.data["total_deleted"] = cls.total_deleted()
|
||||
cls.data["total_changed"] = cls.total_changed()
|
||||
cls.data["changes"] = cls.changes()
|
||||
cls.data["saved_searches"] = cls.saved_searches()
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
@@ -84,3 +85,18 @@ class DiffData:
|
||||
elif element.GlobalId in diff["deleted"]:
|
||||
changes["Deleted"] = True
|
||||
return changes
|
||||
|
||||
@classmethod
|
||||
def saved_searches(cls):
|
||||
if not tool.Ifc.get():
|
||||
return []
|
||||
groups = tool.Ifc.get().by_type("IfcGroup")
|
||||
results = []
|
||||
for group in groups:
|
||||
try:
|
||||
data = json.loads(group.Description)
|
||||
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
|
||||
results.append(group)
|
||||
except:
|
||||
pass
|
||||
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
|
||||
|
||||
@@ -135,6 +135,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
|
||||
bl_label = "Execute IFC Diff"
|
||||
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")
|
||||
@@ -162,7 +163,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
|
||||
new = ifcopenshell.open(self.props.new_file)
|
||||
|
||||
relationships = [r.relationship for r in self.props.diff_relationships]
|
||||
query = self.props.diff_filter_elements
|
||||
query = tool.Search.export_filter_query(self.props.filter_groups) or None
|
||||
|
||||
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query)
|
||||
ifc_diff.diff()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from blenderbim.bim.prop import StrProperty, BIMFilterGroup
|
||||
from blenderbim.bim.module.diff.data import DiffData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -48,7 +48,7 @@ class DiffProperties(PropertyGroup):
|
||||
old_file: StringProperty(default="", name="Old IFC File")
|
||||
new_file: StringProperty(default="", name="New IFC File")
|
||||
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
|
||||
diff_filter_elements: StringProperty(default="", name="Filter")
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True)
|
||||
active_file: EnumProperty(
|
||||
items=[
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.diff.data import DiffData
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
import json
|
||||
|
||||
|
||||
class BIM_PT_diff(Panel):
|
||||
@@ -93,9 +93,7 @@ class BIM_PT_diff(Panel):
|
||||
remove.collection = "diff_relationships"
|
||||
remove.index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "diff_filter_elements")
|
||||
row.operator("bim.ifc_selector", icon="FILTER", text="")
|
||||
blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, DiffData, "diff")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.execute_ifc_diff")
|
||||
|
||||
@@ -41,6 +41,7 @@ classes = (
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
operator.ContractTargetView,
|
||||
operator.ConvertSVGToDXF,
|
||||
operator.CreateDrawing,
|
||||
operator.CreateSheets,
|
||||
operator.DisableAddAnnotationType,
|
||||
@@ -126,7 +127,7 @@ def register():
|
||||
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
|
||||
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
||||
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
|
||||
|
||||
@@ -140,6 +141,6 @@ def unregister():
|
||||
del bpy.types.Object.BIMAssignedProductProperties
|
||||
del bpy.types.Object.BIMTextProperties
|
||||
del bpy.types.TextCurve.BIMTextProperties
|
||||
bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad)
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
|
||||
bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button)
|
||||
|
||||
@@ -606,7 +606,7 @@ class BaseDecorator:
|
||||
for literal_data in literals_data:
|
||||
box_alignment = literal_data["BoxAlignment"]
|
||||
|
||||
for line in literal_data["CurrentValue"].split("\\n"):
|
||||
for line in literal_data["CurrentValue"].split("\n"):
|
||||
self.draw_label(
|
||||
context,
|
||||
line,
|
||||
|
||||
@@ -23,7 +23,7 @@ from bpy.app.handlers import persistent
|
||||
|
||||
|
||||
@persistent
|
||||
def toggleDecorationsOnLoad(*args):
|
||||
def load_post(*args):
|
||||
if bpy.context.scene.DocProperties.should_draw_decorations:
|
||||
decoration.DecorationsHandler.install(bpy.context)
|
||||
else:
|
||||
|
||||
@@ -188,7 +188,6 @@ def format_distance(
|
||||
feet += 1
|
||||
inches = 0
|
||||
|
||||
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches
|
||||
tx_dist = ""
|
||||
@@ -299,7 +298,9 @@ def parse_diagram_scale(camera):
|
||||
return float(numerator) / float(denominator)
|
||||
|
||||
|
||||
def ortho_view_frame(camera, margin=0.015):
|
||||
def ortho_view_frame(
|
||||
camera: bpy.types.Camera, margin: float = 0.015
|
||||
) -> tuple[float, float, float, float, float, float]:
|
||||
"""Calculates 2d bounding box of camera view area.
|
||||
|
||||
Similar to `bpy.types.Camera.view_frame`
|
||||
|
||||
@@ -29,6 +29,7 @@ import subprocess
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
import ifcopenshell
|
||||
import ifcopenshell.ifcopenshell_wrapper
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.selector
|
||||
import ifcopenshell.util.representation
|
||||
@@ -44,7 +45,7 @@ import blenderbim.bim.module.drawing.helper as helper
|
||||
import blenderbim.bim.export_ifc
|
||||
from blenderbim.bim.module.drawing.decoration import CutDecorator
|
||||
from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData
|
||||
from typing import NamedTuple, List
|
||||
from typing import NamedTuple, List, Union, Optional
|
||||
from lxml import etree
|
||||
from mathutils import Vector, Color, Matrix
|
||||
from timeit import default_timer as timer
|
||||
@@ -199,6 +200,11 @@ class CreateDrawing(bpy.types.Operator):
|
||||
+ "SHIFT+CLICK to print all selected drawings"
|
||||
)
|
||||
print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"})
|
||||
sync: bpy.props.BoolProperty(
|
||||
name="Sync Before Creating Drawing",
|
||||
description="Could save some time if you're sure IFC and current Blender session are already in sync",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -214,13 +220,15 @@ class CreateDrawing(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
self.props = context.scene.DocProperties
|
||||
|
||||
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
|
||||
if self.print_all:
|
||||
original_drawing_id = self.props.active_drawing_id
|
||||
original_drawing_id = active_drawing_id
|
||||
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
|
||||
else:
|
||||
drawings_to_print = [self.props.active_drawing_id]
|
||||
drawings_to_print = [active_drawing_id]
|
||||
|
||||
for drawing_id in drawings_to_print:
|
||||
for drawing_i, drawing_id in enumerate(drawings_to_print):
|
||||
self.drawing_index = drawing_i
|
||||
if self.print_all:
|
||||
bpy.ops.bim.activate_drawing(drawing=drawing_id, camera_view_point=False)
|
||||
|
||||
@@ -335,6 +343,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
|
||||
return
|
||||
svg_path = self.get_svg_path(cache_type="underlay")
|
||||
if os.path.isfile(svg_path) and self.props.should_use_underlay_cache:
|
||||
return svg_path
|
||||
|
||||
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
|
||||
for obj in bpy.context.view_layer.objects:
|
||||
obj.hide_render = obj.name not in visible_object_names
|
||||
|
||||
context.scene.render.filepath = svg_path[0:-4] + ".png"
|
||||
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
|
||||
|
||||
@@ -445,8 +460,14 @@ class CreateDrawing(bpy.types.Operator):
|
||||
return LineworkContexts(body_contexts, annotation_contexts)
|
||||
|
||||
def serialize_contexts_elements(
|
||||
self, ifc, tree: ifcopenshell.geom.tree, contexts: LineworkContexts, context_type, drawing_elements, target_view
|
||||
):
|
||||
self,
|
||||
ifc: ifcopenshell.file,
|
||||
tree: ifcopenshell.geom.tree,
|
||||
contexts: LineworkContexts,
|
||||
context_type: str,
|
||||
drawing_elements: set[ifcopenshell.entity_instance],
|
||||
target_view: str,
|
||||
) -> None:
|
||||
drawing_elements = drawing_elements.copy()
|
||||
contexts = getattr(contexts, context_type)
|
||||
for context in contexts:
|
||||
@@ -472,20 +493,24 @@ class CreateDrawing(bpy.types.Operator):
|
||||
tree.add_element(elem)
|
||||
drawing_elements -= processed
|
||||
|
||||
def generate_linework(self, context):
|
||||
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
|
||||
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
|
||||
return
|
||||
svg_path = self.get_svg_path(cache_type="linework")
|
||||
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
||||
return svg_path
|
||||
|
||||
with profile("sync"):
|
||||
# All very hackish whilst prototyping
|
||||
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
|
||||
exporter.file = tool.Ifc.get()
|
||||
invalidated_elements = exporter.sync_all_objects()
|
||||
invalidated_elements += exporter.sync_edited_objects()
|
||||
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
|
||||
# in case of printing multiple drawings we need to sync just once
|
||||
if self.sync and self.drawing_index == 0:
|
||||
with profile("sync"):
|
||||
# All very hackish whilst prototyping
|
||||
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
|
||||
exporter.file = tool.Ifc.get()
|
||||
invalidated_elements = exporter.sync_all_objects(skip_unlinking=True)
|
||||
invalidated_elements += exporter.sync_edited_objects()
|
||||
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
|
||||
cache = IfcStore.get_cache()
|
||||
[cache.remove(guid) for guid in invalidated_guids]
|
||||
|
||||
# If we have already calculated it in the SVG in the past, don't recalculate
|
||||
edited_guids = set()
|
||||
@@ -506,6 +531,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
for ifc_path, ifc in files.items():
|
||||
# Don't use draw.main() just whilst we're prototyping and experimenting
|
||||
# TODO: hash paths are never used
|
||||
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
|
||||
ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
|
||||
|
||||
@@ -518,8 +544,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
|
||||
|
||||
self.setup_serialiser(ifc, target_view)
|
||||
cache = IfcStore.get_cache()
|
||||
[cache.remove(guid) for guid in invalidated_guids]
|
||||
tree = ifcopenshell.geom.tree()
|
||||
tree.enable_face_styles(True)
|
||||
|
||||
@@ -1115,14 +1139,14 @@ class CreateDrawing(bpy.types.Operator):
|
||||
continue
|
||||
return space
|
||||
|
||||
def get_material_name(self, element):
|
||||
def get_material_name(self, element: ifcopenshell.entity_instance) -> str:
|
||||
if hasattr(element, "Name") and element.Name:
|
||||
return element.Name
|
||||
elif hasattr(element, "LayerSetName") and element.LayerSetName:
|
||||
return element.LayerSetName
|
||||
return "mat-" + str(element.id())
|
||||
|
||||
def get_svg_path(self, cache_type=None):
|
||||
def get_svg_path(self, cache_type: Optional[str] = None) -> str:
|
||||
drawing_path = tool.Drawing.get_document_uri(self.camera_document)
|
||||
drawings_dir = os.path.dirname(drawing_path)
|
||||
|
||||
@@ -1219,12 +1243,14 @@ class AddDrawingToSheet(bpy.types.Operator, Operator):
|
||||
return
|
||||
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
|
||||
attributes = {
|
||||
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
|
||||
"Location": drawing_reference.Location,
|
||||
"Description": "DRAWING",
|
||||
}
|
||||
attributes = tool.Drawing.generate_reference_attributes(
|
||||
reference,
|
||||
Identification=str(
|
||||
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
|
||||
),
|
||||
Location=drawing_reference.Location,
|
||||
Description="DRAWING",
|
||||
)
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
|
||||
@@ -1292,9 +1318,10 @@ class CreateSheets(bpy.types.Operator, Operator):
|
||||
|
||||
has_sheet_reference = False
|
||||
for reference in tool.Drawing.get_document_references(sheet):
|
||||
if reference.Description == "SHEET":
|
||||
reference_description = tool.Drawing.get_reference_description(reference)
|
||||
if reference_description == "SHEET":
|
||||
has_sheet_reference = True
|
||||
elif reference.Description == "RASTER":
|
||||
elif reference_description == "RASTER":
|
||||
if reference.Location in raster_references:
|
||||
raster_references.remove(reference.Location)
|
||||
else:
|
||||
@@ -1305,7 +1332,9 @@ class CreateSheets(bpy.types.Operator, Operator):
|
||||
tool.Ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=reference,
|
||||
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
|
||||
attributes=tool.Drawing.generate_reference_attributes(
|
||||
reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET"
|
||||
),
|
||||
)
|
||||
|
||||
for raster_reference in raster_references:
|
||||
@@ -1313,7 +1342,9 @@ class CreateSheets(bpy.types.Operator, Operator):
|
||||
tool.Ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=reference,
|
||||
attributes={"Location": tool.Ifc.get_relative_uri(raster_reference), "Description": "RASTER"},
|
||||
attributes=tool.Drawing.generate_reference_attributes(
|
||||
reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER"
|
||||
),
|
||||
)
|
||||
|
||||
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
|
||||
@@ -1422,13 +1453,19 @@ class ActivateModel(bpy.types.Operator):
|
||||
|
||||
CutDecorator.uninstall()
|
||||
|
||||
# save current visibility statuses for Views and Types collections
|
||||
visibility_status: dict[bpy.types.Object, bool] = {}
|
||||
for col in bpy.data.collections["Views"].children:
|
||||
for obj in col.objects:
|
||||
visibility_status[obj] = obj.hide_get()
|
||||
for obj in bpy.data.collections["Types"].objects:
|
||||
visibility_status[obj] = obj.hide_get()
|
||||
|
||||
if not bpy.app.background:
|
||||
with context.temp_override(**tool.Blender.get_viewport_context()):
|
||||
bpy.ops.object.hide_view_clear()
|
||||
bpy.ops.bim.activate_status_filters()
|
||||
|
||||
subcontext = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
for obj in context.visible_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
@@ -1446,6 +1483,11 @@ class ActivateModel(bpy.types.Operator):
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
|
||||
# restore visibility after hide_view_clear()
|
||||
for obj, hide_status in visibility_status.items():
|
||||
obj.hide_set(hide_status)
|
||||
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1596,8 +1638,14 @@ class ReloadDrawingStyles(bpy.types.Operator):
|
||||
|
||||
json_path = Path(tool.Ifc.resolve_uri(rel_path))
|
||||
if not json_path.exists():
|
||||
self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path))
|
||||
return {"CANCELLED"}
|
||||
ootb_resource = Path(context.scene.BIMProperties.data_dir) / "assets" / "shading_styles.json"
|
||||
print(
|
||||
f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. "
|
||||
f"Default BBIM resource will be copied from {ootb_resource}"
|
||||
)
|
||||
if ootb_resource.exists():
|
||||
os.makedirs(json_path.parent, exist_ok=True)
|
||||
shutil.copy(ootb_resource, json_path)
|
||||
|
||||
with open(json_path, "r") as fi:
|
||||
shading_styles_json = json.load(fi)
|
||||
@@ -1960,12 +2008,14 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
|
||||
return
|
||||
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
|
||||
attributes = {
|
||||
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
|
||||
"Location": schedule_location,
|
||||
"Description": "SCHEDULE",
|
||||
}
|
||||
attributes = tool.Drawing.generate_reference_attributes(
|
||||
reference,
|
||||
Identification=str(
|
||||
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
|
||||
),
|
||||
Location=schedule_location,
|
||||
Description="SCHEDULE",
|
||||
)
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
@@ -2014,12 +2064,15 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
|
||||
return
|
||||
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
|
||||
attributes = {
|
||||
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "REFERENCE")]) + 1),
|
||||
"Location": extref_location,
|
||||
"Description": "REFERENCE",
|
||||
}
|
||||
attributes = tool.Drawing.generate_reference_attributes(
|
||||
reference,
|
||||
Identification=str(
|
||||
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "REFERENCE")])
|
||||
+ 1
|
||||
),
|
||||
Location=extref_location,
|
||||
Description="REFERENCE",
|
||||
)
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
@@ -2345,6 +2398,7 @@ class LoadSheets(bpy.types.Operator, Operator):
|
||||
if not filepath.is_file():
|
||||
sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}"
|
||||
sheets_not_found.append(f'"{sheet_name}" - {document_uri}')
|
||||
core.regenerate_sheet(tool.Drawing, sheet)
|
||||
|
||||
if sheets_not_found:
|
||||
self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found))
|
||||
@@ -2363,8 +2417,8 @@ class EditSheet(bpy.types.Operator, Operator):
|
||||
if sheet.is_a("IfcDocumentInformation"):
|
||||
self.document_type = "SHEET"
|
||||
self.name = sheet.Name
|
||||
self.identification = sheet.Identification
|
||||
elif sheet.is_a("IfcDocumentReference") and sheet.Description == "TITLEBLOCK":
|
||||
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
|
||||
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
|
||||
self.document_type = "TITLEBLOCK"
|
||||
else:
|
||||
self.document_type = "EMBEDDED"
|
||||
@@ -2390,7 +2444,7 @@ class EditSheet(bpy.types.Operator, Operator):
|
||||
if self.document_type == "SHEET":
|
||||
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
|
||||
elif self.document_type == "EMBEDDED":
|
||||
core.rename_reference(tool.Ifc, reference=sheet, identification=self.identification)
|
||||
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
|
||||
elif self.document_type == "TITLEBLOCK":
|
||||
titleblock = self.props.titleblock
|
||||
reference = sheet
|
||||
@@ -2542,8 +2596,16 @@ class EnableEditingElementFilter(bpy.types.Operator, Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.context.scene.camera
|
||||
if obj:
|
||||
obj.data.BIMCameraProperties.filter_mode = self.filter_mode
|
||||
if not obj:
|
||||
return
|
||||
obj.data.BIMCameraProperties.filter_mode = self.filter_mode
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
|
||||
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
|
||||
try:
|
||||
tool.Search.import_filter_query(query, filter_groups)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class EditElementFilter(bpy.types.Operator, Operator):
|
||||
@@ -2685,3 +2747,50 @@ class AddReferenceImage(bpy.types.Operator, Operator):
|
||||
)
|
||||
tool.Style.reload_material_from_ifc(material)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
|
||||
class ConvertSVGToDXF(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_svg_to_dxf"
|
||||
bl_label = "Convert SVG to DXF"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
view: bpy.props.StringProperty()
|
||||
bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings"
|
||||
convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# convert all drawings on shift+click
|
||||
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.open_all = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
if self.convert_all:
|
||||
drawings = [
|
||||
tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected
|
||||
]
|
||||
else:
|
||||
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
|
||||
|
||||
drawing_uris: list[Path] = []
|
||||
drawings_not_found: list[str] = []
|
||||
|
||||
for drawing in drawings:
|
||||
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
|
||||
if drawing_uri is None or not os.path.exists(drawing_uri):
|
||||
drawings_not_found.append(drawing.Name)
|
||||
else:
|
||||
drawing_uris.append(Path(drawing_uri))
|
||||
|
||||
if drawings_not_found:
|
||||
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
|
||||
"\n".join(drawings_not_found)
|
||||
)
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
for drawing_uri in drawing_uris:
|
||||
tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf"))
|
||||
|
||||
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -66,6 +66,11 @@ def get_location_hint(self, context):
|
||||
|
||||
|
||||
def update_diagram_scale(self, context):
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.scene.camera)
|
||||
if not element:
|
||||
return
|
||||
try:
|
||||
element = (
|
||||
tool.Ifc.get()
|
||||
@@ -87,6 +92,11 @@ def update_diagram_scale(self, context):
|
||||
|
||||
|
||||
def update_is_nts(self, context):
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.scene.camera)
|
||||
if not element:
|
||||
return
|
||||
try:
|
||||
element = (
|
||||
tool.Ifc.get()
|
||||
@@ -194,7 +204,7 @@ def update_document_name(self, context):
|
||||
def update_has_underlay(self, context):
|
||||
update_layer(self, context, "HasUnderlay", self.has_underlay)
|
||||
# making sure that camera is active
|
||||
if self.has_underlay and (context.active_object and context.active_object.data == self.id_data):
|
||||
if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data):
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
|
||||
@@ -208,10 +218,12 @@ def update_has_annotation(self, context):
|
||||
|
||||
|
||||
def update_layer(self, context, name, value):
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.scene.camera)
|
||||
if not element:
|
||||
return
|
||||
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
|
||||
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
@@ -229,9 +241,8 @@ def update_titleblocks(self, context):
|
||||
SheetsData.data["titleblocks"] = SheetsData.titleblocks()
|
||||
|
||||
|
||||
def toggleDecorations(self, context):
|
||||
toggle = self.should_draw_decorations
|
||||
if toggle:
|
||||
def update_should_draw_decorations(self, context):
|
||||
if self.should_draw_decorations:
|
||||
# TODO: design a proper text variable templating renderer
|
||||
collection = context.scene.camera.BIMObjectProperties.collection
|
||||
for obj in collection.objects:
|
||||
@@ -343,7 +354,7 @@ class DocProperties(PropertyGroup):
|
||||
active_sheet_index: IntProperty(name="Active Sheet Index")
|
||||
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
|
||||
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
|
||||
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
|
||||
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=update_should_draw_decorations)
|
||||
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
|
||||
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
|
||||
titleblocks_dir: StringProperty(
|
||||
|
||||
@@ -41,7 +41,7 @@ class SheetBuilder:
|
||||
self.data_dir = None
|
||||
self.scale = "NTS"
|
||||
|
||||
def create(self, layout_path, titleblock_name):
|
||||
def create(self, layout_path: str, titleblock_name: str) -> None:
|
||||
root = ET.Element("svg")
|
||||
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
|
||||
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
|
||||
@@ -76,7 +76,12 @@ class SheetBuilder:
|
||||
with open(layout_path, "w") as f:
|
||||
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
|
||||
|
||||
def add_drawing(self, reference, drawing, sheet):
|
||||
def add_drawing(
|
||||
self,
|
||||
reference: ifcopenshell.entity_instance,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
sheet: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
filename = drawing.Name
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
layout_dir = os.path.dirname(layout_path)
|
||||
@@ -131,7 +136,7 @@ class SheetBuilder:
|
||||
)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def update_sheet_drawing_sizes(self, sheet):
|
||||
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
@@ -171,7 +176,7 @@ class SheetBuilder:
|
||||
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def remove_drawing(self, reference, sheet):
|
||||
def remove_drawing(self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance) -> None:
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
@@ -187,7 +192,12 @@ class SheetBuilder:
|
||||
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def add_document(self, reference, document, sheet):
|
||||
def add_document(
|
||||
self,
|
||||
reference: ifcopenshell.entity_instance,
|
||||
document: ifcopenshell.entity_instance,
|
||||
sheet: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(document), "svg")
|
||||
if not os.path.exists(view_path):
|
||||
tool.Drawing.create_svg_document(document)
|
||||
@@ -224,7 +234,7 @@ class SheetBuilder:
|
||||
)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def add_view_title(self, x, y, parent, layout_dir):
|
||||
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
|
||||
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
|
||||
os.makedirs(os.path.dirname(title_path), exist_ok=True)
|
||||
if not os.path.exists(title_path):
|
||||
@@ -241,7 +251,7 @@ class SheetBuilder:
|
||||
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
|
||||
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
|
||||
|
||||
def build(self, sheet):
|
||||
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
|
||||
self.references = {"SHEET": None, "RASTER": []}
|
||||
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
@@ -272,7 +282,7 @@ class SheetBuilder:
|
||||
|
||||
return self.references
|
||||
|
||||
def build_titleblock(self, root, sheet):
|
||||
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
|
||||
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
|
||||
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
|
||||
g = self.parse_embedded_svg(image, sheet.get_info())
|
||||
@@ -285,7 +295,7 @@ class SheetBuilder:
|
||||
titleblock.append(g)
|
||||
titleblock.remove(image)
|
||||
|
||||
def ensure_drawing_unique_styles(self, svg, drawing_id):
|
||||
def ensure_drawing_unique_styles(self, svg: ET.Element, drawing_id: int) -> ET.Element:
|
||||
"""ensures all drawing's classes and ids will be unique for the whole sheet
|
||||
by adding `drawing_id` based prefix
|
||||
"""
|
||||
@@ -313,7 +323,7 @@ class SheetBuilder:
|
||||
brackets_level -= 1
|
||||
text += l
|
||||
|
||||
def replace_urls(text):
|
||||
def replace_urls(text: str) -> str:
|
||||
"""replace urls `url(#marker)` with `url(#prefix-marker)`
|
||||
since `url(#marker.prefix)` doesn't seem to work
|
||||
"""
|
||||
@@ -343,7 +353,7 @@ class SheetBuilder:
|
||||
|
||||
return svg
|
||||
|
||||
def build_drawings(self, root, sheet):
|
||||
def build_drawings(self, root: ET.Element, sheet: ifcopenshell.entity_instance):
|
||||
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
|
||||
drawing_id = int(view.attrib["data-id"])
|
||||
try:
|
||||
@@ -390,7 +400,7 @@ class SheetBuilder:
|
||||
for image in images:
|
||||
view.remove(image)
|
||||
|
||||
def build_documents(self, root, sheet):
|
||||
def build_documents(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
|
||||
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
|
||||
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
|
||||
documents = schedules + references
|
||||
@@ -427,10 +437,10 @@ class SheetBuilder:
|
||||
for image in images:
|
||||
view.remove(image)
|
||||
|
||||
def get_href(self, element):
|
||||
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace('\\','/')
|
||||
def get_href(self, element: ET.Element) -> str:
|
||||
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace("\\", "/")
|
||||
|
||||
def parse_embedded_svg(self, image, data):
|
||||
def parse_embedded_svg(self, image: ET.Element, data: dict) -> ET.Element:
|
||||
group = ET.Element("g")
|
||||
group.attrib["transform"] = "translate({},{})".format(
|
||||
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
|
||||
@@ -466,7 +476,7 @@ class SheetBuilder:
|
||||
group.append(child)
|
||||
return group
|
||||
|
||||
def change_titleblock(self, sheet, titleblock_name):
|
||||
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
|
||||
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
@@ -499,7 +509,7 @@ class SheetBuilder:
|
||||
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def convert_to_mm(self, value):
|
||||
def convert_to_mm(self, value: str) -> float:
|
||||
# CSS is what defines these possibilities
|
||||
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
|
||||
# https://www.w3.org/TR/css-values-3/#absolute-lengths
|
||||
@@ -520,5 +530,5 @@ class SheetBuilder:
|
||||
return float(value[0:-2]) * (1 / 96) * 2.54 * 10
|
||||
return float(value)
|
||||
|
||||
def mm_to_px(self, value):
|
||||
def mm_to_px(self, value: float) -> float:
|
||||
return (value / 25.4) * 96
|
||||
|
||||
@@ -93,7 +93,7 @@ class SvgWriter:
|
||||
|
||||
def setup_drawing_resource_paths(self, element):
|
||||
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
|
||||
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"):
|
||||
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"):
|
||||
resource_path = pset.get(resource)
|
||||
if not resource_path:
|
||||
self.resource_paths[resource] = None
|
||||
@@ -719,14 +719,15 @@ class SvgWriter:
|
||||
self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="ELEVATION", **text_style)
|
||||
)
|
||||
|
||||
def get_reference_and_sheet_id_from_annotation(self, element):
|
||||
def get_reference_and_sheet_id_from_annotation(self, element: ifcopenshell.entity_instance) -> tuple[str, str]:
|
||||
reference_id = "-"
|
||||
sheet_id = "-"
|
||||
drawing = tool.Drawing.get_annotation_element(element)
|
||||
reference = tool.Drawing.get_drawing_reference(drawing)
|
||||
if reference:
|
||||
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
|
||||
if sheet_reference.Description != "DRAWING" or sheet_reference.Location != reference.Location:
|
||||
reference_description = tool.Drawing.get_reference_description(sheet_reference)
|
||||
if reference_description != "DRAWING" or sheet_reference.Location != reference.Location:
|
||||
continue
|
||||
sheet = tool.Drawing.get_reference_document(sheet_reference)
|
||||
if sheet:
|
||||
|
||||
@@ -268,6 +268,11 @@ class BIM_PT_drawings(Panel):
|
||||
col = row.column()
|
||||
col.alignment = "RIGHT"
|
||||
|
||||
convert_to_dxf = row.row(align=True)
|
||||
op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA")
|
||||
op.view = active_drawing.name
|
||||
convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0
|
||||
|
||||
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
|
||||
|
||||
open_drawing_button = row.row(align=True)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.util.placement
|
||||
from mathutils import Vector
|
||||
@@ -222,7 +223,7 @@ class ConnectionsData:
|
||||
@classmethod
|
||||
def is_connection_realization(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
connections = element.IsConnectionRealization
|
||||
connections = getattr(element, "IsConnectionRealization", None)
|
||||
if not connections:
|
||||
return
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ import bmesh
|
||||
import mathutils
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
import blenderbim.tool as tool
|
||||
from math import pi, pow
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
from typing import Union
|
||||
|
||||
|
||||
class Helper:
|
||||
def __init__(self, file):
|
||||
def __init__(self, file: ifcopenshell.file):
|
||||
self.file = file
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
|
||||
@@ -34,7 +36,7 @@ class Helper:
|
||||
# edge that shares a single vertex only with that face to find the extrusion
|
||||
# edge. A face with the normal facing down is prioritised. A limited
|
||||
# dissolve ensure that faces are quads and not tris.
|
||||
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
|
||||
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
@@ -115,7 +117,9 @@ class Helper:
|
||||
|
||||
return {"profile": profile, "extrusion": extrusion}
|
||||
|
||||
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
|
||||
def auto_detect_arbitrary_profile_with_voids(
|
||||
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
|
||||
) -> Union[tuple, dict]:
|
||||
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
|
||||
for i, group in enumerate(obj.vertex_groups):
|
||||
if "IFCARCINDEX" in group.name:
|
||||
@@ -138,28 +142,31 @@ class Helper:
|
||||
total_groups = 0
|
||||
is_circle = False
|
||||
for group_type, group_indices in groups.items():
|
||||
for group_index in group_indices:
|
||||
if group_index in vert[deform_layer]:
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
total_groups += 0
|
||||
if not group_indices:
|
||||
continue
|
||||
is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices)
|
||||
if not is_special:
|
||||
continue
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
total_groups += 0
|
||||
if total_groups > 1: # A vert can only belong to one group
|
||||
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
|
||||
elif is_circle:
|
||||
pass # Circles are allowed to be unclosed
|
||||
pass # Circles are allowed to be unclosed
|
||||
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
|
||||
return (False, "UNCLOSED_LOOP")
|
||||
|
||||
for group_type, group_counts in group_verts.items():
|
||||
if group_type == "IFCARCINDEX":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
elif group_type == "IFCCIRCLE":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
loop_edges = set(bm.edges)
|
||||
@@ -269,7 +276,7 @@ class Helper:
|
||||
inner_loops.remove(outer_loop)
|
||||
|
||||
# Copy vectors to prevent random data mangling after bmesh is freed.
|
||||
points = [Vector(list(v.co)) for v in bm.verts]
|
||||
points = [v.co.copy() for v in bm.verts]
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
@@ -531,7 +538,7 @@ class Helper:
|
||||
)
|
||||
position = None
|
||||
if self.file.schema == "IFC2X3":
|
||||
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0, 0.0]))
|
||||
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0]))
|
||||
curve = self.file.createIfcRectangleProfileDef("AREA", None, position, xdim, ydim)
|
||||
return {"curve_ucs": curve_ucs, "curve": curve}
|
||||
|
||||
|
||||
@@ -215,6 +215,13 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
|
||||
disable_opening_subtractions: bpy.props.BoolProperty()
|
||||
should_switch_all_meshes: bpy.props.BoolProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if context.active_object.mode == "OBJECT":
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode - Press TAB in the viewport")
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
target_representation = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
target = target_representation.ContextOfItems
|
||||
@@ -223,6 +230,8 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if not obj.mode == "OBJECT":
|
||||
continue
|
||||
if obj == context.active_object:
|
||||
representation = target_representation
|
||||
else:
|
||||
@@ -324,8 +333,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
# We are explicitly casting to a tessellation, so remove all parametric materials.
|
||||
element_type = ifcopenshell.util.element.get_type(product)
|
||||
if element_type: # Some invalid IFCs use material sets without a type.
|
||||
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type)
|
||||
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=product)
|
||||
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[element_type])
|
||||
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[product])
|
||||
else:
|
||||
# These objects are parametrically based on an axis and should not be modified as a mesh
|
||||
return
|
||||
@@ -538,6 +547,8 @@ class OverrideDelete(bpy.types.Operator):
|
||||
row.prop(self, "is_batch", text="Enable Faster Deletion")
|
||||
|
||||
def _execute(self, context):
|
||||
start_time = time()
|
||||
|
||||
if self.is_batch:
|
||||
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
|
||||
|
||||
@@ -562,6 +573,11 @@ class OverrideDelete(bpy.types.Operator):
|
||||
IfcStore.add_transaction_operation(self)
|
||||
# Required otherwise gizmos are still visible
|
||||
context.view_layer.objects.active = None
|
||||
|
||||
operator_time = time() - start_time
|
||||
if operator_time > 10:
|
||||
self.report({"INFO"}, "IFC Delete was finished in {:.2f} seconds".format(operator_time))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def rollback(self, data):
|
||||
@@ -843,7 +859,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
|
||||
OverrideDuplicateMove.handle_linked_aggregates(old_to_new)
|
||||
OverrideDuplicateMove.remove_linked_aggregate_data(old_to_new)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return old_to_new
|
||||
|
||||
@@ -896,25 +912,21 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
|
||||
@staticmethod
|
||||
def handle_linked_aggregates(old_to_new):
|
||||
def remove_linked_aggregate_data(old_to_new):
|
||||
for old, new in old_to_new.items():
|
||||
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
|
||||
if pset:
|
||||
old_aggregate = ifcopenshell.util.element.get_aggregate(old)
|
||||
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
|
||||
if old_aggregate == new_aggregate:
|
||||
parts = ifcopenshell.util.element.get_parts(new_aggregate)
|
||||
if parts:
|
||||
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
|
||||
index += 1
|
||||
pset = tool.Ifc.get().by_id(pset['id'])
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Index": index},
|
||||
)
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
|
||||
if new[0].is_a("IfcElementAssembly"):
|
||||
linked_aggregate_group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[new[0]])
|
||||
|
||||
|
||||
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
|
||||
@@ -940,6 +952,7 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
|
||||
|
||||
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
|
||||
bl_description = "Create a new linked aggregate"
|
||||
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
|
||||
bl_label = "IFC Duplicate Linked Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -974,34 +987,33 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
obj.select_set(True)
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
|
||||
index = get_max_index(parts)
|
||||
add_linked_aggregate_pset(element, index)
|
||||
index +=1
|
||||
index += 1
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
select_objects_and_add_data(part)
|
||||
else:
|
||||
add_linked_aggregate_pset(part, index)
|
||||
index += 1
|
||||
|
||||
index = add_linked_aggregate_pset(part, index)
|
||||
# index += 1
|
||||
|
||||
obj = tool.Ifc.get_object(part)
|
||||
obj.select_set(True)
|
||||
|
||||
|
||||
|
||||
def add_linked_aggregate_pset(part, index):
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
|
||||
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
|
||||
)
|
||||
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Index": index},
|
||||
)
|
||||
|
||||
index += 1
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -1029,21 +1041,53 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
][0]
|
||||
|
||||
|
||||
number = len(group_elements) - 1
|
||||
number = f"{number:02d}"
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
pattern1 = r'_\d'
|
||||
pattern1 = r"_\d"
|
||||
if re.findall(pattern1, new_obj.name):
|
||||
split_name = new_obj.name.split("_")
|
||||
new_obj.name = split_name[0] + "_" + number
|
||||
continue
|
||||
pattern2 = r'\.\d{3}'
|
||||
pattern2 = r"\.\d{3}"
|
||||
if re.findall(pattern2, new_obj.name):
|
||||
split_name = new_obj.name.split(".")
|
||||
new_obj.name = split_name[0] + "_" + number
|
||||
|
||||
|
||||
def get_max_index(parts):
|
||||
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
|
||||
index = [i["Index"] for i in psets if i]
|
||||
if len(index) > 0:
|
||||
index = max(index)
|
||||
return index
|
||||
else:
|
||||
return 0
|
||||
|
||||
def copy_linked_aggregate_data(old_to_new):
|
||||
for old, new in old_to_new.items():
|
||||
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
|
||||
if pset:
|
||||
new_pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=new[0], name=self.pset_name
|
||||
)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=new_pset,
|
||||
properties={"Index": pset["Index"]},
|
||||
)
|
||||
|
||||
if new[0].is_a("IfcElementAssembly"):
|
||||
linked_aggregate_group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(old, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new)
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1064,29 +1108,16 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
copy_linked_aggregate_data(old_to_new)
|
||||
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
# Recreate aggregate relationship
|
||||
for old in old_to_new.keys():
|
||||
if old.is_a("IfcElementAssembly"):
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
return old_to_new
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_max_index(parts):
|
||||
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
|
||||
index = [i['Index'] for i in psets if i]
|
||||
if len(index) > 0:
|
||||
index = max(index)
|
||||
return index
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.refresh_linked_aggregate"
|
||||
@@ -1134,9 +1165,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
original_names[group] = {}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset['Index']
|
||||
index = pset["Index"]
|
||||
original_names[group][index] = tool.Ifc.get_object(element).name
|
||||
|
||||
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
for part in parts:
|
||||
@@ -1144,20 +1175,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
original_names | get_original_names(part)
|
||||
else:
|
||||
try:
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
except:
|
||||
continue
|
||||
index = pset['Index']
|
||||
continue
|
||||
index = pset["Index"]
|
||||
original_names[group][index] = tool.Ifc.get_object(part).name
|
||||
|
||||
|
||||
return original_names
|
||||
|
||||
def set_original_name(obj, original_names):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
if ifcopenshell.util.element.get_parts(
|
||||
element
|
||||
): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
aggregate = element
|
||||
|
||||
|
||||
group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
@@ -1166,18 +1199,16 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
]
|
||||
if not group:
|
||||
return
|
||||
|
||||
|
||||
group = group[0].id()
|
||||
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset['Index']
|
||||
|
||||
index = pset["Index"]
|
||||
|
||||
try:
|
||||
obj.name = original_names[group][index]
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
|
||||
def get_element_assembly(element):
|
||||
if element.is_a("IfcElementAssembly"):
|
||||
@@ -1220,6 +1251,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
return list(set(linked_aggregate_groups)), selected_parents
|
||||
|
||||
def get_original_matrix(element, base_instance):
|
||||
selected_obj = tool.Ifc.get_object(base_instance)
|
||||
selected_matrix = selected_obj.matrix_world
|
||||
object_duplicate = tool.Ifc.get_object(element)
|
||||
duplicate_matrix = object_duplicate.matrix_world.decompose()
|
||||
|
||||
return selected_matrix, duplicate_matrix
|
||||
|
||||
def set_new_matrix(selected_matrix, duplicate_matrix, old_to_new):
|
||||
for old, new in old_to_new.items():
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
|
||||
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
|
||||
new_obj_matrix = new_base_matrix @ matrix_diff
|
||||
new_obj.matrix_world = new_obj_matrix
|
||||
|
||||
active_element = tool.Ifc.get_entity(context.active_object)
|
||||
if not active_element:
|
||||
self.report({"INFO"}, "Object has no Ifc metadata.")
|
||||
@@ -1259,13 +1306,10 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
element_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
|
||||
selected_obj = tool.Ifc.get_object(base_instance)
|
||||
selected_matrix = selected_obj.matrix_world
|
||||
object_duplicate = tool.Ifc.get_object(element)
|
||||
duplicate_matrix = object_duplicate.matrix_world.decompose()
|
||||
|
||||
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance)
|
||||
|
||||
original_names = get_original_names(element)
|
||||
|
||||
|
||||
delete_objects(element)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
@@ -1274,30 +1318,26 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
tool.Ifc.get_object(base_instance).select_set(True)
|
||||
|
||||
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
|
||||
for old, new in old_to_new.items():
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
|
||||
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
|
||||
new_obj_matrix = new_base_matrix @ matrix_diff
|
||||
new_obj.matrix_world = new_obj_matrix
|
||||
|
||||
|
||||
set_new_matrix(selected_matrix, duplicate_matrix, old_to_new)
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
if element_aggregate and new[0].is_a("IfcElementAssembly"):
|
||||
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
|
||||
|
||||
if not new_aggregate:
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
set_original_name(new_obj, original_names)
|
||||
|
||||
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
operator_time = time() - refresh_start_time
|
||||
@@ -1411,14 +1451,13 @@ class OverridePasteBuffer(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.pastebuffer()
|
||||
if IfcStore.get_file():
|
||||
for obj in context.selected_objects:
|
||||
# Pasted objects may come from another Blender session, or even
|
||||
# from the same session where the original object has since
|
||||
# been deleted. As the source element may not exist, paste will
|
||||
# always unlink the element. If you want to duplicate an
|
||||
# element, use the duplicate commands.
|
||||
tool.Root.unlink_object(obj)
|
||||
for obj in context.selected_objects:
|
||||
# Pasted objects may come from another Blender session, or even
|
||||
# from the same session where the original object has since
|
||||
# been deleted. As the source element may not exist, paste will
|
||||
# always unlink the element. If you want to duplicate an
|
||||
# element, use the duplicate commands.
|
||||
tool.Root.unlink_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1445,7 +1484,9 @@ class OverrideModeSetEdit(bpy.types.Operator):
|
||||
for obj in selected_objs:
|
||||
if not obj:
|
||||
continue
|
||||
if not obj.data:
|
||||
obj_supports_edit_mode, message = tool.Blender.object_supports_edit_mode(obj)
|
||||
if not obj_supports_edit_mode:
|
||||
self.report({"INFO"}, message)
|
||||
obj.select_set(False)
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
@@ -212,7 +212,7 @@ class BIM_PT_representation_items(Panel):
|
||||
if props.is_editing_item_style:
|
||||
# NOTE: we currently support 1 item having just 1 style
|
||||
# when IfcStyledItem can actually have multiple styles
|
||||
row.prop(props, "representation_item_style", icon="SHADING_RENDERED", text="")
|
||||
prop_with_search(row, props, "representation_item_style", icon="SHADING_RENDERED", text="")
|
||||
row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="")
|
||||
else:
|
||||
|
||||
@@ -212,7 +212,7 @@ class UnassignGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"group.unassign_group",
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id),
|
||||
"products": [self.file.by_id(product.BIMObjectProperties.ifc_definition_id)],
|
||||
"group": self.file.by_id(self.group),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -151,7 +151,10 @@ class AssignPresentationLayer(bpy.types.Operator):
|
||||
ifcopenshell.api.run(
|
||||
"layer.assign_layer",
|
||||
self.file,
|
||||
**{"item": self.file.by_id(item.BIMMeshProperties.ifc_definition_id), "layer": self.file.by_id(self.layer)}
|
||||
**{
|
||||
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
|
||||
"layer": self.file.by_id(self.layer),
|
||||
}
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -173,7 +176,10 @@ class UnassignPresentationLayer(bpy.types.Operator):
|
||||
ifcopenshell.api.run(
|
||||
"layer.unassign_layer",
|
||||
self.file,
|
||||
**{"item": self.file.by_id(item.BIMMeshProperties.ifc_definition_id), "layer": self.file.by_id(self.layer)}
|
||||
**{
|
||||
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
|
||||
"layer": self.file.by_id(self.layer),
|
||||
}
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ classes = (
|
||||
operator.AddLayer,
|
||||
operator.AddListItem,
|
||||
operator.AddMaterial,
|
||||
operator.DuplicateMaterial,
|
||||
operator.AddMaterialSet,
|
||||
operator.AddProfile,
|
||||
operator.AssignMaterial,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import os
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.doc
|
||||
import ifcopenshell.util.schema
|
||||
import blenderbim.tool as tool
|
||||
@@ -166,6 +167,9 @@ class ObjectMaterialData:
|
||||
cls.data["type_material"] = cls.type_material()
|
||||
cls.data["material_type"] = cls.material_type()
|
||||
cls.data["active_material_constituents"] = cls.active_material_constituents()
|
||||
# after material_name and type_material
|
||||
cls.data["is_type_material_overridden"] = cls.is_type_material_overridden()
|
||||
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
@@ -294,8 +298,7 @@ class ObjectMaterialData:
|
||||
|
||||
@classmethod
|
||||
def material_name(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
material = cls.material
|
||||
if material:
|
||||
return getattr(material, "Name", None) or "Unnamed"
|
||||
|
||||
@@ -339,3 +342,18 @@ class ObjectMaterialData:
|
||||
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
|
||||
return []
|
||||
return [m.Name for m in material.MaterialConstituents if m.Name]
|
||||
|
||||
@classmethod
|
||||
def is_type_material_overridden(cls) -> bool:
|
||||
if not cls.data["type_material"]:
|
||||
return False
|
||||
|
||||
# try to avoid accessing ifc
|
||||
if cls.data["material_name"] != cls.data["type_material"]:
|
||||
return True
|
||||
|
||||
# in theory material can be overridden by the same material
|
||||
# so we check occurrence material explicitly
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
occurrence_material = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
||||
return bool(occurrence_material)
|
||||
|
||||
@@ -118,7 +118,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_material"
|
||||
bl_label = "Add Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(name="Material Name")
|
||||
name: bpy.props.StringProperty(default="Default")
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -134,6 +134,19 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
material_prop_purge()
|
||||
|
||||
|
||||
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.duplicate_material"
|
||||
bl_label = "Diplicate Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty(name="Material ID")
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
tool.Material.duplicate_material(ifc_file.by_id(self.material))
|
||||
material_prop_purge()
|
||||
bpy.ops.bim.load_materials()
|
||||
|
||||
|
||||
class AddMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_material_set"
|
||||
bl_label = "Add Material Set"
|
||||
|
||||
@@ -61,6 +61,8 @@ class BIM_PT_materials(Panel):
|
||||
if self.props.materials and self.props.active_material_index < len(self.props.materials):
|
||||
material = self.props.materials[self.props.active_material_index]
|
||||
if material.ifc_definition_id:
|
||||
op = row.operator("bim.duplicate_material", text="", icon="DUPLICATE")
|
||||
op.material = material.ifc_definition_id
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL")
|
||||
@@ -175,7 +177,13 @@ class BIM_PT_object_material(Panel):
|
||||
|
||||
if ObjectMaterialData.data["type_material"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT")
|
||||
if ObjectMaterialData.data["is_type_material_overridden"]:
|
||||
row.label(
|
||||
text=f"Inherited Material Is Occurrence Overridden",
|
||||
icon="CON_CHILDOF",
|
||||
)
|
||||
else:
|
||||
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF")
|
||||
|
||||
if ObjectMaterialData.data["material_class"]:
|
||||
return self.draw_material_ui()
|
||||
|
||||
@@ -32,7 +32,9 @@ class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operato
|
||||
def poll(cls, context):
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
return tool.Ifc.get_entity(collection_obj)
|
||||
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
|
||||
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
|
||||
return tool.Ifc.get_entity(collection_obj) and relating_type == "FLOORING"
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -61,7 +63,9 @@ class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator
|
||||
def poll(cls, context):
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
return tool.Ifc.get_entity(collection_obj)
|
||||
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
|
||||
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
|
||||
return tool.Ifc.get_entity(collection_obj) and relating_type == "CEILING"
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -116,9 +120,11 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
|
||||
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
|
||||
if element:
|
||||
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
|
||||
return context.selected_objects
|
||||
return context.selected_objects and relating_type == "FLOORING"
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
@@ -147,7 +153,7 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
|
||||
|
||||
class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_instance_ceiling_coverings_from_walls"
|
||||
bl_label = "Add Ceilings From Walls"
|
||||
bl_label = "Add Ceiling From Walls"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add instance ceiling coverings from selected walls. The active object must be a wall and layered vertically"
|
||||
|
||||
@@ -155,9 +161,11 @@ class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
|
||||
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
|
||||
if element:
|
||||
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
|
||||
return context.selected_objects
|
||||
return context.selected_objects and relating_type == "CEILING"
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
|
||||
@@ -23,6 +23,7 @@ from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from typing import Union
|
||||
|
||||
|
||||
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
|
||||
@@ -35,15 +36,6 @@ def transparent_color(color, alpha=0.1):
|
||||
return color
|
||||
|
||||
|
||||
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
|
||||
"""returns tuple boolean (whether vertex is in any of the groups)
|
||||
and related group index"""
|
||||
for group_index in vertex[deform_layer].keys():
|
||||
if group_index in groups:
|
||||
return True, group_index
|
||||
return False, None
|
||||
|
||||
|
||||
class ProfileDecorator:
|
||||
installed = None
|
||||
|
||||
@@ -148,12 +140,12 @@ class ProfileDecorator:
|
||||
# deform_layer is None if there are no verts assigned to vertex groups
|
||||
# even if there are vertex groups in the obj.vertex_groups
|
||||
if deform_layer:
|
||||
is_arc, group_index = bm_check_vertex_in_groups(vertex, deform_layer, arc_groups)
|
||||
is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups)
|
||||
if is_arc:
|
||||
arcs.setdefault(group_index, []).append(vertex)
|
||||
special_vertex_indices[vertex.index] = group_index
|
||||
|
||||
is_circle, group_index = bm_check_vertex_in_groups(vertex, deform_layer, circle_groups)
|
||||
is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups)
|
||||
if is_circle:
|
||||
circles.setdefault(group_index, []).append(vertex)
|
||||
special_vertex_indices[vertex.index] = group_index
|
||||
|
||||
@@ -542,7 +542,7 @@ class MEPGenerator:
|
||||
ifc_representation_class=None,
|
||||
)
|
||||
|
||||
rel = ifcopenshell.api.run("material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet")
|
||||
rel = ifcopenshell.api.run("material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet")
|
||||
profile_set = rel.RelatingMaterial
|
||||
material_profile = ifcopenshell.api.run(
|
||||
"material.add_profile", ifc_file, profile_set=profile_set, material=material
|
||||
|
||||
@@ -24,10 +24,14 @@ import shapely
|
||||
import logging
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.bim.import_ifc as import_ifc
|
||||
@@ -107,7 +111,7 @@ class FilledOpeningGenerator:
|
||||
# Equivalent to "side Z" for a wall axis, so that stuff like skylights appear on the top.
|
||||
local_position_on_voided_obj.z = layers["offset"] + layers["thickness"]
|
||||
new_matrix.translation.xyz = voided_obj.matrix_world @ local_position_on_voided_obj
|
||||
rotation_matrix = Matrix.Rotation(radians(-90), 4, 'X')
|
||||
rotation_matrix = Matrix.Rotation(radians(-90), 4, "X")
|
||||
new_matrix @= rotation_matrix
|
||||
|
||||
filling_obj.matrix_world = new_matrix
|
||||
@@ -184,7 +188,13 @@ class FilledOpeningGenerator:
|
||||
)
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
filling = settings["related_object"]
|
||||
relating_type = settings["relating_type"]
|
||||
|
||||
for related_object in settings["related_objects"]:
|
||||
self._regenerate_from_type(related_object)
|
||||
|
||||
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
|
||||
filling = related_object
|
||||
if not getattr(filling, "FillsVoids", None):
|
||||
return
|
||||
|
||||
|
||||
@@ -25,9 +25,13 @@ import ifcopenshell.util.system
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.aggregate
|
||||
import blenderbim.core.type
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.core.spatial
|
||||
from . import wall, slab, profile, mep
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
@@ -35,6 +39,7 @@ from mathutils import Vector, Matrix
|
||||
from bpy_extras.object_utils import AddObjectHelper
|
||||
from . import prop
|
||||
import json
|
||||
from typing import Any, Union
|
||||
|
||||
|
||||
class EnableAddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -273,7 +278,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
obj.location[2] = collection_obj.location[2] + bpy.context.scene.BIMModelProperties.rl2
|
||||
|
||||
@staticmethod
|
||||
def generate_layered_element(ifc_class, relating_type):
|
||||
def generate_layered_element(ifc_class: str, relating_type: ifcopenshell.entity_instance) -> bool:
|
||||
layer_set_direction = None
|
||||
|
||||
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
|
||||
@@ -297,6 +302,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
material = ifcopenshell.util.element.get_material(tool.Ifc.get_entity(obj))
|
||||
material.LayerSetDirection = layer_set_direction
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -506,11 +512,11 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
|
||||
)
|
||||
|
||||
|
||||
def ensure_material_assigned(usecase_path, ifc_file, settings):
|
||||
def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||
if usecase_path == "material.assign_material":
|
||||
if not settings.get("material", None):
|
||||
return
|
||||
elements = [settings["product"]]
|
||||
elements = settings["products"]
|
||||
else:
|
||||
elements = []
|
||||
for rel in ifc_file.by_type("IfcRelAssociatesMaterial"):
|
||||
@@ -519,53 +525,76 @@ def ensure_material_assigned(usecase_path, ifc_file, settings):
|
||||
]:
|
||||
elements.extend(rel.RelatedObjects)
|
||||
|
||||
for element in elements:
|
||||
obj = IfcStore.get_element(element.GlobalId)
|
||||
if not obj or not obj.data:
|
||||
continue
|
||||
|
||||
element_material = ifcopenshell.util.element.get_material(element)
|
||||
material = [m for m in ifc_file.traverse(element_material) if m.is_a("IfcMaterial")]
|
||||
|
||||
object_material_ids = [
|
||||
om.BIMObjectProperties.ifc_definition_id
|
||||
for om in obj.data.materials
|
||||
if om is not None and om.BIMObjectProperties.ifc_definition_id
|
||||
]
|
||||
|
||||
if material and material[0].id() in object_material_ids:
|
||||
continue
|
||||
|
||||
if len(obj.data.materials) == 1:
|
||||
obj.data.materials.clear()
|
||||
|
||||
if not material:
|
||||
continue
|
||||
|
||||
obj.data.materials.append(IfcStore.get_element(material[0].id()))
|
||||
update_blender_ifc_materials(elements)
|
||||
|
||||
|
||||
def ensure_material_unassigned(usecase_path, ifc_file, settings):
|
||||
elements = [settings["product"]]
|
||||
def ensure_material_unassigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||
elements = settings["products"]
|
||||
if elements[0].is_a("IfcElementType"):
|
||||
elements.extend(ifcopenshell.util.element.get_types(elements[0]))
|
||||
update_blender_ifc_materials(elements)
|
||||
|
||||
|
||||
def update_blender_ifc_materials(elements: list[ifcopenshell.entity_instance]) -> None:
|
||||
"""update mesh blender materials that have ifc material connected to them
|
||||
by replacing them with `blender_material`"""
|
||||
# since different elements can share meshes (e.g. occurrecnes without openings)
|
||||
# we need to make sure not to affect them accidentally
|
||||
meshes_users: dict[bpy.types.Mesh, set[bpy.types.Object]] = dict()
|
||||
for obj in bpy.data.objects:
|
||||
if not obj.data:
|
||||
continue
|
||||
meshes_users.setdefault(obj.data, set()).add(obj)
|
||||
|
||||
objects: set[bpy.types.Object] = set()
|
||||
for element in elements:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj: bpy.types.Object = tool.Ifc.get_object(element)
|
||||
if not obj or not obj.data:
|
||||
continue
|
||||
element_material = ifcopenshell.util.element.get_material(element)
|
||||
if element_material:
|
||||
objects.add(obj)
|
||||
|
||||
meshes: set[bpy.types.Mesh] = {obj.data for obj in objects}
|
||||
|
||||
for mesh in meshes:
|
||||
mesh_users = meshes_users[mesh]
|
||||
if not mesh_users.issubset(objects):
|
||||
continue
|
||||
to_remove = []
|
||||
for i, slot in enumerate(obj.material_slots):
|
||||
if not slot.material:
|
||||
|
||||
# NOTE: we need `obj` as removing materials and appending them to `mesh.materials`
|
||||
# will mess up mesh faces material indices
|
||||
|
||||
# NOTE: we make an assumption here that all mesh users
|
||||
# have the same material - they either inherit it from the type
|
||||
# or type doesn't have a material.
|
||||
#
|
||||
# If we add option to UI to add materials overriding type materials
|
||||
# then this assumption won't be safe anymore
|
||||
|
||||
obj = next(iter(mesh_users))
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
current_material = ifcopenshell.util.element.get_material(element)
|
||||
if current_material:
|
||||
current_material = tool.Ifc.get_object(current_material)
|
||||
|
||||
material_replaced = False
|
||||
|
||||
for material_slot in obj.material_slots:
|
||||
material = material_slot.material
|
||||
if material is None:
|
||||
continue
|
||||
material = tool.Ifc.get_entity(slot.material)
|
||||
if material:
|
||||
to_remove.append(i)
|
||||
total_removed = 0
|
||||
for i in to_remove:
|
||||
obj.active_material_index = i - total_removed
|
||||
with bpy.context.temp_override(object=obj):
|
||||
bpy.ops.object.material_slot_remove()
|
||||
total_removed += 1
|
||||
ifc_material = tool.Ifc.get_entity(material)
|
||||
# it's blender material for style, so ignore it
|
||||
if not ifc_material:
|
||||
continue
|
||||
if ifc_material == current_material:
|
||||
continue
|
||||
material_slot.material = current_material
|
||||
material_replaced = True
|
||||
|
||||
if not material_replaced and current_material:
|
||||
mesh.materials.append(current_material)
|
||||
|
||||
# clear empty slots
|
||||
for i, material in reversed(list(enumerate(mesh.materials[:]))):
|
||||
if material is None:
|
||||
mesh.materials.pop(index=i)
|
||||
|
||||
@@ -21,14 +21,18 @@ import copy
|
||||
import bmesh
|
||||
import mathutils.geometry
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.type
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.core.material
|
||||
import blenderbim.core.root
|
||||
from math import pi, degrees, inf
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from blenderbim.bim.module.geometry.helper import Helper
|
||||
@@ -92,7 +96,7 @@ class DumbProfileGenerator:
|
||||
should_add_representation=False,
|
||||
context=self.body_context,
|
||||
)
|
||||
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
|
||||
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
|
||||
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
material.CardinalPoint = self.cardinal_point
|
||||
@@ -207,12 +211,19 @@ class DumbProfileRegenerator:
|
||||
return results
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
obj = tool.Ifc.get_object(settings["related_object"])
|
||||
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
return
|
||||
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
|
||||
relating_type = settings["relating_type"]
|
||||
|
||||
new_material = ifcopenshell.util.element.get_material(relating_type)
|
||||
if not new_material or not new_material.is_a("IfcMaterialProfileSet"):
|
||||
return
|
||||
|
||||
for related_object in settings["related_objects"]:
|
||||
self._regenerate_from_type(related_object)
|
||||
|
||||
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
|
||||
obj = tool.Ifc.get_object(related_object)
|
||||
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
return
|
||||
DumbProfileRecalculator().recalculate([obj])
|
||||
|
||||
|
||||
|
||||
@@ -20,17 +20,22 @@ import bpy
|
||||
import json
|
||||
import bmesh
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.type
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.core.type
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.core.root
|
||||
import blenderbim.tool as tool
|
||||
from math import radians
|
||||
from mathutils import Vector, Matrix
|
||||
from blenderbim.bim.module.geometry.helper import Helper
|
||||
from blenderbim.bim.module.model.decorator import ProfileDecorator
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def calculate_quantities(usecase_path, ifc_file, settings):
|
||||
@@ -110,7 +115,7 @@ def calculate_quantities(usecase_path, ifc_file, settings):
|
||||
|
||||
|
||||
class DumbSlabGenerator:
|
||||
def __init__(self, relating_type):
|
||||
def __init__(self, relating_type: ifcopenshell.entity_instance):
|
||||
self.relating_type = relating_type
|
||||
|
||||
def generate(self):
|
||||
@@ -173,7 +178,7 @@ class DumbSlabGenerator:
|
||||
should_add_representation=False,
|
||||
context=self.body_context,
|
||||
)
|
||||
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
|
||||
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
|
||||
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
representation = ifcopenshell.api.run(
|
||||
@@ -246,27 +251,38 @@ class DumbSlabPlaner:
|
||||
self.change_thickness(element, total_thickness)
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
obj = tool.Ifc.get_object(settings["related_object"])
|
||||
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
return
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
|
||||
relating_type = settings["relating_type"]
|
||||
|
||||
new_material = ifcopenshell.util.element.get_material(relating_type)
|
||||
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
|
||||
return
|
||||
|
||||
parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric")
|
||||
layer_set_direction = None
|
||||
if parametric:
|
||||
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
|
||||
new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers])
|
||||
material = ifcopenshell.util.element.get_material(settings["related_object"])
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
for related_object in settings["related_objects"]:
|
||||
self._regenerate_from_type(related_object, layer_set_direction, new_thickness)
|
||||
|
||||
def _regenerate_from_type(
|
||||
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str], new_thickness: float
|
||||
) -> None:
|
||||
obj = tool.Ifc.get_object(related_object)
|
||||
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
return
|
||||
|
||||
material = ifcopenshell.util.element.get_material(related_object)
|
||||
if not material or not material.is_a("IfcMaterialLayerSetUsage"):
|
||||
return
|
||||
if layer_set_direction:
|
||||
material.LayerSetDirection = layer_set_direction
|
||||
if material.LayerSetDirection == "AXIS3":
|
||||
self.change_thickness(settings["related_object"], new_thickness)
|
||||
self.change_thickness(related_object, new_thickness)
|
||||
|
||||
def change_thickness(self, element, thickness):
|
||||
def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None:
|
||||
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj:
|
||||
@@ -698,9 +714,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
new_footprint = ifcopenshell.api.run(
|
||||
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
|
||||
if old_footprint:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_footprint):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
|
||||
|
||||
@@ -27,7 +27,7 @@ import blenderbim.core.type
|
||||
|
||||
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.generate_space"
|
||||
bl_label = "Generate Space"
|
||||
bl_label = "Generate Space from Cursor"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = (
|
||||
"Create a space from the cursor position. "
|
||||
@@ -35,30 +35,34 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"select the right space collection and run the operator"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
active_obj = context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# print(context)
|
||||
# collection = context.view_layer.active_layer_collection.collection
|
||||
# collection_obj = collection.BIMCollectionProperties.obj
|
||||
# active_obj = context.active_object
|
||||
# element = tool.Ifc.get_entity(active_obj)
|
||||
# return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
|
||||
|
||||
def _execute(self, context):
|
||||
# This works as a 2.5 extruded polygon based on a cutting plane. Note
|
||||
# that rooms exclude walls (i.e. not to wall midpoint or exterior /
|
||||
# exterior edge.
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY")
|
||||
def msg_no_collection(self, context):
|
||||
self.layout.label(text="NO ACTIVE COLLECTION. PLEASE SELECT A SPATIAL COLLECTION OBJECT OR A WALL")
|
||||
|
||||
def msg_no_active_storey(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY. PLEASE SELECT A SPATIAL COLLECTION OBJECT")
|
||||
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
if not collection_obj:
|
||||
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
context.window_manager.popup_menu(msg_no_collection, title="Error", icon="ERROR")
|
||||
return
|
||||
spatial_element = tool.Ifc.get_entity(collection_obj)
|
||||
if not spatial_element:
|
||||
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
if not spatial_element or not spatial_element.is_a("IfcBuildingStorey"):
|
||||
context.window_manager.popup_menu(msg_no_active_storey, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type)
|
||||
@@ -70,12 +74,12 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Generate spaces from selected walls. The active object must be a wall"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element:
|
||||
return context.selected_objects and element.is_a("IfcWall")
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# active_obj = context.active_object
|
||||
# element = tool.Ifc.get_entity(active_obj)
|
||||
# if element:
|
||||
# return context.selected_objects and element.is_a("IfcWall")
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
@@ -87,19 +91,30 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
container = tool.Spatial.get_container(element)
|
||||
|
||||
def msg_no_active_object(self, context):
|
||||
self.layout.label(text="No active object. Please select a wall")
|
||||
def msg_no_active_wall(self, context):
|
||||
self.layout.label(text="The active object is not a wall. Please select a wall.")
|
||||
def msg_no_container(self, context):
|
||||
self.layout.label(text="The wall is not contained. Please the selected wall in a building container")
|
||||
def msg_no_selected_objects(self, context):
|
||||
self.layout.label(text="No selected objects found. Please select walls.")
|
||||
|
||||
if not active_obj:
|
||||
self.report({"ERROR"}, "No active object. Please select a wall")
|
||||
context.window_manager.popup_menu(msg_no_active_object, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element and not element.is_a("IfcWall"):
|
||||
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
|
||||
context.window_manager.popup_menu(msg_no_active_wall, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
if not container:
|
||||
self.report({"ERROR"}, "The wall is not contained.")
|
||||
context.window_manager.popup_menu(msg_no_container, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
if not context.selected_objects:
|
||||
self.report({"ERROR"}, "No selected objects found. Please select walls.")
|
||||
context.window_manager.popup_menu(msg_no_selected_objects, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
|
||||
|
||||
@@ -54,6 +54,11 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
props.type_page = 1
|
||||
if get_ifc_class(None, context):
|
||||
ifc_class = props.ifc_class or AuthoringData.data["ifc_element_type"]
|
||||
else:
|
||||
ifc_class = AuthoringData.data["ifc_element_type"]
|
||||
|
||||
# will be None if project has no types
|
||||
if ifc_class is not None:
|
||||
props.type_class = ifc_class
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
|
||||
return context.window_manager.invoke_popup(self, width=550)
|
||||
|
||||
@@ -24,7 +24,9 @@ import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.type
|
||||
import mathutils.geometry
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.core.type
|
||||
@@ -35,6 +37,7 @@ from blenderbim.bim.ifc import IfcStore
|
||||
from math import pi, sin, cos, degrees, radians
|
||||
from mathutils import Vector, Matrix
|
||||
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -555,7 +558,10 @@ class DumbWallGenerator:
|
||||
for face in sibling_obj.data.polygons:
|
||||
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
|
||||
face_center = sibling_obj.matrix_world @ face.center
|
||||
if normal.z != 0 or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01:
|
||||
if (
|
||||
normal.z != 0
|
||||
or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01
|
||||
):
|
||||
continue
|
||||
|
||||
rotation = math.atan2(normal[1], normal[0])
|
||||
@@ -603,7 +609,7 @@ class DumbWallGenerator:
|
||||
should_add_representation=False,
|
||||
context=self.body_context,
|
||||
)
|
||||
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
|
||||
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
|
||||
if self.axis_context:
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_axis_representation",
|
||||
@@ -724,18 +730,29 @@ class DumbWallPlaner:
|
||||
DumbWallRecalculator().recalculate([w for w in set(walls) if w])
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
obj = tool.Ifc.get_object(settings["related_object"])
|
||||
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
return
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
|
||||
relating_type = settings["relating_type"]
|
||||
|
||||
new_material = ifcopenshell.util.element.get_material(relating_type)
|
||||
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
|
||||
return
|
||||
parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric")
|
||||
|
||||
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
|
||||
layer_set_direction = None
|
||||
if parametric:
|
||||
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
|
||||
material = ifcopenshell.util.element.get_material(settings["related_object"])
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
for related_object in settings["related_objects"]:
|
||||
self._regenerate_from_type(related_object, layer_set_direction)
|
||||
|
||||
def _regenerate_from_type(
|
||||
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str]
|
||||
) -> None:
|
||||
obj = tool.Ifc.get_object(related_object)
|
||||
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
return
|
||||
|
||||
material = ifcopenshell.util.element.get_material(related_object)
|
||||
if not material or not material.is_a("IfcMaterialLayerSetUsage"):
|
||||
return
|
||||
if layer_set_direction:
|
||||
@@ -854,11 +871,11 @@ class DumbWallJoiner:
|
||||
if (location_on_base - location).length < (location_on_side - location).length:
|
||||
axis_offset = location_on_side - location_on_base
|
||||
offset_from_axis = location_on_base - location
|
||||
opening_matrix.translation = (location_on_base - axis_offset - offset_from_axis)
|
||||
opening_matrix.translation = location_on_base - axis_offset - offset_from_axis
|
||||
else:
|
||||
axis_offset = location_on_side - location_on_base
|
||||
offset_from_axis = location_on_side - location
|
||||
opening_matrix.translation = (location_on_side - axis_offset - offset_from_axis)
|
||||
opening_matrix.translation = location_on_side - axis_offset - offset_from_axis
|
||||
opening_matrixes[opening] = opening_matrix
|
||||
|
||||
for filling in [r.RelatedBuildingElement for r in opening.HasFillings]:
|
||||
@@ -871,11 +888,11 @@ class DumbWallJoiner:
|
||||
if (location_on_base - location).length < (location_on_side - location).length:
|
||||
axis_offset = location_on_side - location_on_base
|
||||
offset_from_axis = location_on_base - location
|
||||
filling_matrix.translation = (location_on_base - axis_offset - offset_from_axis)
|
||||
filling_matrix.translation = location_on_base - axis_offset - offset_from_axis
|
||||
else:
|
||||
axis_offset = location_on_side - location_on_base
|
||||
offset_from_axis = location_on_side - location
|
||||
filling_matrix.translation = (location_on_side - axis_offset - offset_from_axis)
|
||||
filling_matrix.translation = location_on_side - axis_offset - offset_from_axis
|
||||
filling_matrixes[filling] = filling_matrix
|
||||
|
||||
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
||||
@@ -1120,13 +1137,8 @@ class DumbWallJoiner:
|
||||
other = tool.Ifc.get_object(rel.RelatedElement)
|
||||
if connection not in ["ATPATH", "NOTDEFINED"]:
|
||||
self.join(
|
||||
obj,
|
||||
other,
|
||||
connection,
|
||||
rel.RelatedConnectionType,
|
||||
is_relating=True,
|
||||
description=rel.Description
|
||||
)
|
||||
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
|
||||
)
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a("IfcRelConnectsPathElements"):
|
||||
connection = rel.RelatedConnectionType
|
||||
|
||||
@@ -378,6 +378,7 @@ class BimToolUI:
|
||||
op.depth = cls.props.extrusion_depth
|
||||
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
|
||||
|
||||
if AuthoringData.data["active_class"] in (
|
||||
"IfcCableCarrierSegment",
|
||||
@@ -385,8 +386,8 @@ class BimToolUI:
|
||||
"IfcDuctSegment",
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
|
||||
|
||||
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "")
|
||||
if context.region.type != "TOOL_HEADER":
|
||||
cls.layout.operator("bim.mep_add_bend")
|
||||
cls.layout.operator("bim.mep_add_transition")
|
||||
@@ -394,7 +395,6 @@ class BimToolUI:
|
||||
|
||||
else:
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
|
||||
@@ -522,11 +522,12 @@ class BimToolUI:
|
||||
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
|
||||
row = cls.layout.row()
|
||||
row.prop(cls.props, "type_name")
|
||||
row = cls.layout.row()
|
||||
row = cls.layout.row(align=True)
|
||||
op = row.operator(
|
||||
"bim.add_default_type", icon="ADD", text=f"Add {AuthoringData.data['ifc_element_type']}"
|
||||
)
|
||||
op.ifc_element_type = AuthoringData.data["ifc_element_type"]
|
||||
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
|
||||
else:
|
||||
row.label(text="No Element Types Found", icon="ERROR")
|
||||
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
|
||||
@@ -718,10 +719,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.flip_wall()
|
||||
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
|
||||
bpy.ops.bim.flip_fill()
|
||||
elif self.active_class in ("IfcBeam", "IfcColumn"):
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
bpy.ops.bim.flip_object(flip_local_axes="XZ")
|
||||
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
|
||||
bpy.ops.bim.fit_flow_segments()
|
||||
|
||||
|
||||
def hotkey_S_G(self):
|
||||
obj = bpy.context.active_object
|
||||
@@ -807,9 +807,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
if self.active_material_usage == "LAYER2":
|
||||
bpy.ops.bim.join_wall(join_type="V")
|
||||
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
|
||||
bpy.ops.bim.fit_flow_segments()
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
bpy.ops.bim.extend_profile(join_type="V")
|
||||
|
||||
|
||||
def hotkey_S_B(self):
|
||||
bpy.ops.bim.add_boundary()
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from . import ui, prop, operator, data
|
||||
|
||||
classes = (
|
||||
operator.AddProfileDef,
|
||||
operator.DuplicateProfileDef,
|
||||
operator.DisableEditingArbitraryProfile,
|
||||
operator.DisableEditingProfile,
|
||||
operator.DisableProfileEditingUI,
|
||||
|
||||
@@ -66,9 +66,15 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator):
|
||||
profile: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
current_index = props.active_profile_index
|
||||
ifcopenshell.api.run("profile.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile))
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
# preserve selected index if possible
|
||||
if props.profiles:
|
||||
props.active_profile_index = min(current_index, len(props.profiles) - 1)
|
||||
|
||||
|
||||
class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_profile"
|
||||
@@ -126,6 +132,27 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
|
||||
class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.duplicate_profile_def"
|
||||
bl_label = "Duplicate Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
if len(props.profiles) > props.active_profile_index:
|
||||
return True
|
||||
cls.poll_message_set("No profile selected to duplicate.")
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
profile = ifc_file.by_id(props.profiles[props.active_profile_index].ifc_definition_id)
|
||||
tool.Profile.duplicate_profile(profile)
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
|
||||
class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_arbitrary_profile"
|
||||
bl_label = "Enable Editing Arbitrary Profile"
|
||||
|
||||
@@ -70,6 +70,7 @@ class BIM_PT_profiles(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "profile_classes", text="")
|
||||
row.operator("bim.add_profile_def", text="", icon="ADD")
|
||||
row.operator("bim.duplicate_profile_def", icon="DUPLICATE", text="")
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_profiles",
|
||||
|
||||
@@ -25,6 +25,8 @@ classes = (
|
||||
operator.AppendLibraryElement,
|
||||
operator.AppendLibraryElementByQuery,
|
||||
operator.AssignLibraryDeclaration,
|
||||
operator.BIM_OT_load_clipping_planes,
|
||||
operator.BIM_OT_save_clipping_planes,
|
||||
operator.ChangeLibraryElement,
|
||||
operator.CreateClippingPlane,
|
||||
operator.CreateProject,
|
||||
|
||||
@@ -21,6 +21,7 @@ import bpy
|
||||
import time
|
||||
import logging
|
||||
import tempfile
|
||||
import traceback
|
||||
import subprocess
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
@@ -39,12 +40,15 @@ from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.ui import IFCFileSelector
|
||||
from blenderbim.bim import import_ifc
|
||||
from blenderbim.bim import export_ifc
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from math import radians
|
||||
from pathlib import Path
|
||||
from mathutils import Vector, Matrix
|
||||
from bpy.app.handlers import persistent
|
||||
from blenderbim.bim.module.project.data import LinksData
|
||||
from blenderbim.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
|
||||
from typing import Union
|
||||
|
||||
|
||||
class NewProject(bpy.types.Operator):
|
||||
@@ -113,6 +117,10 @@ class CreateProject(bpy.types.Operator):
|
||||
if tool.Blender.is_default_scene():
|
||||
for obj in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
for mesh in bpy.data.meshes:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
for mat in bpy.data.materials:
|
||||
bpy.data.materials.remove(mat)
|
||||
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
|
||||
tool.Blender.register_toolbar()
|
||||
|
||||
@@ -295,7 +303,7 @@ class AssignLibraryDeclaration(bpy.types.Operator):
|
||||
ifcopenshell.api.run(
|
||||
"project.assign_declaration",
|
||||
self.file,
|
||||
definition=self.file.by_id(self.definition),
|
||||
definitions=[self.file.by_id(self.definition)],
|
||||
relating_context=self.file.by_type("IfcProjectLibrary")[0],
|
||||
)
|
||||
element_name = self.props.active_library_element
|
||||
@@ -331,7 +339,7 @@ class UnassignLibraryDeclaration(bpy.types.Operator):
|
||||
ifcopenshell.api.run(
|
||||
"project.unassign_declaration",
|
||||
self.file,
|
||||
definition=self.file.by_id(self.definition),
|
||||
definitions=[self.file.by_id(self.definition)],
|
||||
relating_context=self.file.by_type("IfcProjectLibrary")[0],
|
||||
)
|
||||
element_name = self.props.active_library_element
|
||||
@@ -667,19 +675,23 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
|
||||
return self.finish_loading_project(context)
|
||||
|
||||
def finish_loading_project(self, context):
|
||||
if not self.is_existing_ifc_file():
|
||||
return {"FINISHED"}
|
||||
try:
|
||||
if not self.is_existing_ifc_file():
|
||||
return {"FINISHED"}
|
||||
|
||||
if tool.Blender.is_default_scene():
|
||||
for obj in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
if tool.Blender.is_default_scene():
|
||||
for obj in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
context.scene.BIMProperties.ifc_file = self.get_filepath()
|
||||
context.scene.BIMProjectProperties.is_loading = True
|
||||
context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
|
||||
tool.Blender.register_toolbar()
|
||||
if not self.is_advanced:
|
||||
bpy.ops.bim.load_project_elements()
|
||||
context.scene.BIMProperties.ifc_file = self.get_filepath()
|
||||
context.scene.BIMProjectProperties.is_loading = True
|
||||
context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
|
||||
tool.Blender.register_toolbar()
|
||||
if not self.is_advanced:
|
||||
bpy.ops.bim.load_project_elements()
|
||||
except:
|
||||
blenderbim.last_error = traceback.format_exc()
|
||||
raise
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -856,9 +868,21 @@ class LinkIfc(bpy.types.Operator):
|
||||
continue
|
||||
new = context.scene.BIMProjectProperties.links.add()
|
||||
if self.use_relative_path:
|
||||
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
|
||||
try:
|
||||
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
|
||||
except:
|
||||
pass # Perhaps on another drive or something
|
||||
new.name = filepath
|
||||
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
|
||||
status = bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
|
||||
if status == {"CANCELLED"}:
|
||||
error_msg = (
|
||||
f'Error processing IFC file "{self.filepath}" '
|
||||
"was critical and blend file either wasn't saved or wasn't updated. "
|
||||
"See logs above in system console for details."
|
||||
)
|
||||
print(error_msg)
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"FINISHED"}
|
||||
print(f"Finished linking {len(files)} IFCs", time.time() - start)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -937,10 +961,12 @@ class LoadLink(bpy.types.Operator):
|
||||
if self.filepath.lower().endswith(".blend"):
|
||||
self.link_blend(filepath)
|
||||
elif self.filepath.lower().endswith(".ifc"):
|
||||
self.link_ifc()
|
||||
status = self.link_ifc()
|
||||
if status:
|
||||
return status
|
||||
return {"FINISHED"}
|
||||
|
||||
def link_blend(self, filepath):
|
||||
def link_blend(self, filepath: str) -> None:
|
||||
with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
|
||||
data_to.scenes = data_from.scenes
|
||||
for scene in bpy.data.scenes:
|
||||
@@ -953,7 +979,7 @@ class LoadLink(bpy.types.Operator):
|
||||
link = bpy.context.scene.BIMProjectProperties.links.get(self.filepath)
|
||||
link.is_loaded = True
|
||||
|
||||
def link_ifc(self):
|
||||
def link_ifc(self) -> Union[set[str], None]:
|
||||
blend_filepath = self.filepath + ".cache.blend"
|
||||
h5_filepath = self.filepath + ".cache.h5"
|
||||
|
||||
@@ -973,11 +999,14 @@ except Exception as e:
|
||||
exit(1)
|
||||
"""
|
||||
|
||||
t = time.time()
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
||||
temp_file.write(code)
|
||||
run = subprocess.run([bpy.app.binary_path, "-b", "--python", temp_file.name, "--python-exit-code", "1"])
|
||||
if run.returncode == 1:
|
||||
print("An error occurred while processing your IFC.")
|
||||
if not os.path.exists(blend_filepath) or os.stat(blend_filepath).st_mtime < t:
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.link_blend(blend_filepath)
|
||||
|
||||
@@ -1287,6 +1316,8 @@ class LoadLinkedProject(bpy.types.Operator):
|
||||
if len(shape.geometry.faces) > 1000 and self.is_local(shape): # 333 tris
|
||||
self.process_occurrence(shape)
|
||||
if not iterator.next():
|
||||
if not chunked_verts:
|
||||
break
|
||||
mats = np.concatenate(chunked_materials)
|
||||
midx = np.concatenate(chunked_material_ids)
|
||||
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
||||
@@ -1322,6 +1353,9 @@ class LoadLinkedProject(bpy.types.Operator):
|
||||
|
||||
ms = np.vstack([default_mat, np.frombuffer(shape.geometry.colors_buffer).reshape((-1, 4))])
|
||||
mi = np.frombuffer(shape.geometry.material_ids_buffer, dtype=np.int32)
|
||||
for geom_material_idx, geom_material in enumerate(shape.geometry.materials):
|
||||
if not geom_material.has_diffuse:
|
||||
ms[geom_material_idx + 1] = (0.8, 0.8, 0.8, 1)
|
||||
chunked_materials.append(ms)
|
||||
chunked_material_ids.append(mi + material_offset + 1)
|
||||
material_offset += len(ms)
|
||||
@@ -1442,7 +1476,10 @@ class LoadLinkedProject(bpy.types.Operator):
|
||||
alpha = 1.0
|
||||
if material.has_transparency and material.transparency > 0:
|
||||
alpha = 1.0 - material.transparency
|
||||
diffuse = material.diffuse + (alpha,)
|
||||
if material.has_diffuse:
|
||||
diffuse = material.diffuse + (alpha,)
|
||||
else:
|
||||
diffuse = (0.8, 0.8, 0.8, 1) # Blender's default material
|
||||
material_name = f"{diffuse[0]}-{diffuse[1]}-{diffuse[2]}-{diffuse[3]}"
|
||||
blender_mat = self.blender_mats.get(material_name, None)
|
||||
if not blender_mat:
|
||||
@@ -1905,32 +1942,32 @@ class CreateClippingPlane(bpy.types.Operator):
|
||||
bl_label = "Create Clipping Plane"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.area.type == "VIEW_3D"
|
||||
|
||||
def execute(self, context):
|
||||
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
|
||||
|
||||
# Clean up deleted planes
|
||||
|
||||
if len(bpy.context.scene.BIMProjectProperties.clipping_planes) > 5:
|
||||
if len(context.scene.BIMProjectProperties.clipping_planes) > 5:
|
||||
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
|
||||
return {"FINISHED"}
|
||||
|
||||
for area in bpy.context.screen.areas:
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
coord = (self.mouse_x, self.mouse_y)
|
||||
origin = region_2d_to_origin_3d(region, rv3d, coord)
|
||||
direction = region_2d_to_vector_3d(region, rv3d, coord)
|
||||
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
|
||||
if not hit:
|
||||
self.report({"INFO"}, "No object found.")
|
||||
return {"FINISHED"}
|
||||
if rv3d: # Called from a 3D viewport
|
||||
coord = (self.mouse_x, self.mouse_y)
|
||||
origin = region_2d_to_origin_3d(region, rv3d, coord)
|
||||
direction = region_2d_to_vector_3d(region, rv3d, coord)
|
||||
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
|
||||
if not hit:
|
||||
self.report({"INFO"}, "No object found.")
|
||||
return {"FINISHED"}
|
||||
else: # Not Called from a 3D viewport
|
||||
location = (0, 0, 1)
|
||||
normal = (0, 0, 1)
|
||||
|
||||
vertices = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)]
|
||||
|
||||
@@ -1941,20 +1978,20 @@ class CreateClippingPlane(bpy.types.Operator):
|
||||
mesh.update()
|
||||
|
||||
plane_obj = bpy.data.objects.new("ClippingPlane", mesh)
|
||||
bpy.context.collection.objects.link(plane_obj)
|
||||
context.collection.objects.link(plane_obj)
|
||||
z_axis = Vector((0, 0, 1))
|
||||
rotation_matrix = z_axis.rotation_difference(normal).to_matrix().to_4x4()
|
||||
plane_obj.matrix_world = rotation_matrix
|
||||
plane_obj.matrix_world.translation = location
|
||||
|
||||
bpy.context.scene.cursor.location = location
|
||||
context.scene.cursor.location = location
|
||||
|
||||
new = bpy.context.scene.BIMProjectProperties.clipping_planes.add()
|
||||
new = context.scene.BIMProjectProperties.clipping_planes.add()
|
||||
new.obj = plane_obj
|
||||
|
||||
tool.Blender.set_active_object(plane_obj)
|
||||
|
||||
ClippingPlaneDecorator.install(bpy.context)
|
||||
ClippingPlaneDecorator.install(context)
|
||||
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1976,11 +2013,69 @@ class FlipClippingPlane(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.area.type == "VIEW_3D"
|
||||
return context.active_object
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.context.active_object
|
||||
if obj in [cp.obj for cp in bpy.context.scene.BIMProjectProperties.clipping_planes]:
|
||||
obj = context.active_object
|
||||
if obj in context.scene.BIMProjectProperties.clipping_planes_objs:
|
||||
obj.rotation_euler[0] += radians(180)
|
||||
bpy.context.view_layer.update()
|
||||
context.view_layer.update()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
CLIPPING_PLANES_FILE_NAME = "ClippingPlanes.json" # TODO un-hardcode :=
|
||||
|
||||
|
||||
class BIM_OT_save_clipping_planes(bpy.types.Operator):
|
||||
bl_idname = "bim.save_clipping_planes"
|
||||
bl_label = "Save Clipping Planes"
|
||||
bl_description = "Save Clipping Planes to Disk"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if IfcStore.path:
|
||||
return context.scene.BIMProjectProperties.clipping_planes
|
||||
cls.poll_message_set("Please Save The IFC File")
|
||||
|
||||
def execute(self, context):
|
||||
clipping_planes_to_serialize = defaultdict(dict)
|
||||
clipping_planes = context.scene.BIMProjectProperties.clipping_planes
|
||||
for clipping_plane in clipping_planes:
|
||||
obj = clipping_plane.obj
|
||||
name = obj.name
|
||||
clipping_planes_to_serialize[name]["location"] = obj.location[0:3]
|
||||
clipping_planes_to_serialize[name]["rotation"] = obj.rotation_euler[0:3]
|
||||
with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "w") as file:
|
||||
json.dump(clipping_planes_to_serialize, file, indent=4)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_load_clipping_planes(bpy.types.Operator):
|
||||
bl_idname = "bim.load_clipping_planes"
|
||||
bl_label = "Load Clipping Planes"
|
||||
bl_description = "Load Clipping Planes from Disk"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if filepath := IfcStore.path:
|
||||
if Path(filepath).with_name(CLIPPING_PLANES_FILE_NAME).exists():
|
||||
return True
|
||||
else:
|
||||
cls.poll_message_set(f"No Clipping Planes File in Folder {filepath}")
|
||||
else:
|
||||
cls.poll_message_set("Please Save The IFC File")
|
||||
|
||||
def execute(self, context):
|
||||
bpy.data.batch_remove(context.scene.BIMProjectProperties.clipping_planes_objs)
|
||||
context.scene.BIMProjectProperties.clipping_planes.clear()
|
||||
with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "r") as file:
|
||||
clipping_planes_dict = json.load(file)
|
||||
for name, values in clipping_planes_dict.items():
|
||||
bpy.ops.bim.create_clipping_plane()
|
||||
obj = context.scene.BIMProjectProperties.clipping_planes_objs[-1]
|
||||
obj.name = name
|
||||
obj.location = values["location"]
|
||||
obj.rotation_euler = values["rotation"]
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -187,6 +187,11 @@ class BIMProjectProperties(PropertyGroup):
|
||||
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
|
||||
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
|
||||
clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
clipping_planes_active: bpy.props.IntProperty(min=0, default=0, max=5)
|
||||
|
||||
@property
|
||||
def clipping_planes_objs(self):
|
||||
return list({cp.obj for cp in self.clipping_planes if cp.obj})
|
||||
|
||||
def get_library_element_index(self, lib_element):
|
||||
return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element))
|
||||
|
||||
@@ -69,6 +69,7 @@ class ObjectPsetsData(Data):
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"is_occurrence": cls.is_occurrence(),
|
||||
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), psets_only=True),
|
||||
"inherited_psets": cls.inherited_psets(),
|
||||
"pset_name": cls.pset_name(),
|
||||
@@ -76,6 +77,10 @@ class ObjectPsetsData(Data):
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def is_occurrence(cls):
|
||||
return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject")
|
||||
|
||||
@classmethod
|
||||
def inherited_psets(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
@@ -126,11 +131,16 @@ class ObjectQtosData(Data):
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"is_occurrence": cls.is_occurrence(),
|
||||
"qtos": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), qtos_only=True),
|
||||
"inherited_qsets": cls.inherited_qsets(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def is_occurrence(cls):
|
||||
return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject")
|
||||
|
||||
@classmethod
|
||||
def inherited_qsets(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
|
||||
@@ -25,8 +25,16 @@ from shapely.geometry import Polygon
|
||||
from shapely.ops import unary_union
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.element
|
||||
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
|
||||
import blenderbim.bim
|
||||
from typing import Literal, Union, Optional
|
||||
|
||||
|
||||
AxisType = Literal["x", "y", "z"]
|
||||
VectorTuple = tuple[float, float, float]
|
||||
QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
|
||||
|
||||
|
||||
class QtoCalculator:
|
||||
@@ -45,7 +53,7 @@ class QtoCalculator:
|
||||
else:
|
||||
self.mapping_dict[key][item] = None
|
||||
|
||||
def calculate_quantity(self, qto_name, quantity_name, obj):
|
||||
def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
|
||||
"""calculates the value of the quantity in the project units"""
|
||||
string = "self.mapping_dict[qto_name][quantity_name](obj"
|
||||
if isinstance(mapper[qto_name][quantity_name], dict):
|
||||
@@ -54,11 +62,13 @@ class QtoCalculator:
|
||||
args = ""
|
||||
string += args
|
||||
string += ")"
|
||||
value = eval(string)
|
||||
value: float = eval(string)
|
||||
|
||||
return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value
|
||||
|
||||
def guess_quantity(self, prop_name, alternative_prop_names, obj):
|
||||
def guess_quantity(
|
||||
self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object
|
||||
) -> Union[float, None]:
|
||||
"""guess the value of the quantity by name, returns the value in the project units"""
|
||||
prop_name = prop_name.lower()
|
||||
alternative_prop_names = [p.lower() for p in alternative_prop_names]
|
||||
@@ -89,7 +99,7 @@ class QtoCalculator:
|
||||
if value is None:
|
||||
return
|
||||
|
||||
unit_type_keywords = {
|
||||
unit_type_keywords: dict[str, QuanityTypes] = {
|
||||
"length": "Q_LENGTH",
|
||||
"width": "Q_LENGTH",
|
||||
"height": "Q_LENGTH",
|
||||
@@ -102,10 +112,10 @@ class QtoCalculator:
|
||||
unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name)
|
||||
return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value
|
||||
|
||||
def get_units(self, o, vg_index):
|
||||
def get_units(self, o: bpy.types.Object, vg_index: int) -> int:
|
||||
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
|
||||
|
||||
def get_linear_length(self, o):
|
||||
def get_linear_length(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the length of the longest edge of the object bounding box
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
@@ -116,7 +126,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x, y, z)
|
||||
|
||||
def get_length(self, o, vg_index=None, main_axis: str = ""):
|
||||
def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float:
|
||||
if vg_index is None:
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
@@ -141,23 +151,23 @@ class QtoCalculator:
|
||||
length += self.get_edge_distance(o, e)
|
||||
return length
|
||||
|
||||
def get_stair_length(self, obj):
|
||||
def get_stair_length(self, obj: bpy.types.Object) -> float:
|
||||
length = self.get_length(obj)
|
||||
height = self.get_height(obj)
|
||||
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
|
||||
return stair_length
|
||||
|
||||
def get_net_stair_area(self, obj):
|
||||
def get_net_stair_area(self, obj: bpy.types.Object) -> float:
|
||||
OBB_obj = self.get_OBB_object(obj)
|
||||
OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj)
|
||||
return OBB_net_footprint_area
|
||||
|
||||
def get_gross_stair_area(self, obj):
|
||||
def get_gross_stair_area(self, obj: bpy.types.Object) -> float:
|
||||
OBB_obj = self.get_OBB_object(obj)
|
||||
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
|
||||
return OBB_gross_footprint_area
|
||||
|
||||
def get_parametric_axis(self, obj):
|
||||
def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
|
||||
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
|
||||
if relating_type:
|
||||
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
|
||||
@@ -172,7 +182,7 @@ class QtoCalculator:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_covering_gross_area(self, obj):
|
||||
def get_covering_gross_area(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_gross_footprint_area(obj)
|
||||
@@ -181,7 +191,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_gross_footprint_area(obj)
|
||||
|
||||
def get_covering_net_area(self, obj):
|
||||
def get_covering_net_area(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_net_footprint_area(obj)
|
||||
@@ -190,7 +200,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_net_footprint_area(obj)
|
||||
|
||||
def get_covering_width(self, obj):
|
||||
def get_covering_width(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_height(obj)
|
||||
@@ -199,7 +209,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_height(obj)
|
||||
|
||||
def get_width(self, o):
|
||||
def get_width(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the width of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -209,7 +219,7 @@ class QtoCalculator:
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
return min(x, y)
|
||||
|
||||
def get_height(self, o):
|
||||
def get_height(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the height of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -217,64 +227,77 @@ class QtoCalculator:
|
||||
"""
|
||||
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
def get_opening_height(self, obj):
|
||||
def get_opening_height(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_width(obj)
|
||||
else:
|
||||
return self.get_height(obj)
|
||||
|
||||
def get_opening_depth(self, obj):
|
||||
def get_opening_depth(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_height(obj)
|
||||
else:
|
||||
return self.get_width(obj)
|
||||
|
||||
def get_opening_mapping_area(self, obj):
|
||||
def get_opening_mapping_area(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_net_footprint_area(obj)
|
||||
else:
|
||||
return self.get_net_side_area(obj)
|
||||
|
||||
def get_finish_ceiling_height(self, obj):
|
||||
space_height = self.get_height(obj)
|
||||
def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float:
|
||||
floor_height = self.get_finish_floor_height(obj)
|
||||
ceiling_height = self.get_ceiling_height(obj)
|
||||
finish_ceiling_height = space_height - floor_height - ceiling_height
|
||||
finish_ceiling_height = ceiling_height - floor_height
|
||||
return finish_ceiling_height
|
||||
|
||||
def get_finish_floor_height(self, obj):
|
||||
def get_max_global_z(self, obj: bpy.types.Object) -> float:
|
||||
z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box]
|
||||
return max(z_values)
|
||||
|
||||
def get_min_global_z(self, obj: bpy.types.Object) -> float:
|
||||
z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box]
|
||||
return min(z_values)
|
||||
|
||||
def get_finish_floor_height(self, obj: bpy.types.Object) -> float:
|
||||
space_min_z_value = self.get_min_global_z(obj)
|
||||
space_max_z_value = self.get_max_global_z(obj)
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
finish_floor_height = 0
|
||||
flooring_max_z_value = space_min_z_value
|
||||
for decomposition in decompositions:
|
||||
if (
|
||||
decomposition.get_info()["PredefinedType"] == "FLOORING"
|
||||
and decomposition.get_info()["type"] == "IfcCovering"
|
||||
decomposition.is_a() == "IfcCovering"
|
||||
and ifcopenshell.util.element.get_predefined_type(decomposition) == "FLOORING"
|
||||
):
|
||||
floor_obj = tool.Ifc.get_object(decomposition)
|
||||
new_finish_floor_height = self.get_height(floor_obj)
|
||||
if new_finish_floor_height > finish_floor_height:
|
||||
finish_floor_height = new_finish_floor_height
|
||||
flooring_obj = tool.Ifc.get_object(decomposition)
|
||||
flooring_z_value = self.get_max_global_z(flooring_obj)
|
||||
if flooring_z_value > space_min_z_value:
|
||||
flooring_max_z_value = flooring_z_value
|
||||
|
||||
return finish_floor_height
|
||||
return flooring_max_z_value - space_min_z_value
|
||||
|
||||
def get_ceiling_height(self, obj: bpy.types.Object) -> float:
|
||||
space_min_z_value = self.get_min_global_z(obj)
|
||||
space_max_z_value = self.get_max_global_z(obj)
|
||||
|
||||
def get_ceiling_height(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
finish_ceiling_height = 0
|
||||
ceiling_min_z_value = space_max_z_value
|
||||
for decomposition in decompositions:
|
||||
if (
|
||||
decomposition.get_info()["PredefinedType"] == "CEILING"
|
||||
and decomposition.get_info()["type"] == "IfcCovering"
|
||||
decomposition.is_a() == "IfcCovering"
|
||||
and ifcopenshell.util.element.get_predefined_type(decomposition) == "CEILING"
|
||||
):
|
||||
ceiling_obj = tool.Ifc.get_object(decomposition)
|
||||
new_finish_ceiling_height = self.get_height(ceiling_obj)
|
||||
if new_finish_ceiling_height > finish_ceiling_height:
|
||||
finish_ceiling_height = new_finish_ceiling_height
|
||||
ceiling_z_value = self.get_min_global_z(ceiling_obj)
|
||||
if ceiling_z_value < space_max_z_value:
|
||||
ceiling_min_z_value = ceiling_z_value
|
||||
|
||||
return finish_ceiling_height
|
||||
return ceiling_min_z_value - space_min_z_value
|
||||
|
||||
def get_net_perimeter(self, o):
|
||||
def get_net_perimeter(self, o: bpy.types.Object) -> float:
|
||||
parsed_edges = []
|
||||
shared_edges = []
|
||||
perimeter = 0
|
||||
@@ -289,7 +312,7 @@ class QtoCalculator:
|
||||
perimeter -= self.get_edge_key_distance(o, edge_key)
|
||||
return perimeter
|
||||
|
||||
def get_gross_perimeter(self, o):
|
||||
def get_gross_perimeter(self, o: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(o)
|
||||
mesh = self.get_gross_element_mesh(element)
|
||||
gross_obj = bpy.data.objects.new("GrossObj", mesh)
|
||||
@@ -297,15 +320,15 @@ class QtoCalculator:
|
||||
self.delete_obj(gross_obj)
|
||||
return gross_perimeter
|
||||
|
||||
def get_space_net_perimeter(self, obj):
|
||||
def get_space_net_perimeter(self, obj: bpy.types.Object) -> float:
|
||||
pass
|
||||
|
||||
def get_rectangular_perimeter(self, obj):
|
||||
def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float:
|
||||
length = self.get_length(obj, main_axis="x")
|
||||
height = self.get_height(obj)
|
||||
return (length + height) * 2
|
||||
|
||||
def get_lowest_polygons(self, o):
|
||||
def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
|
||||
lowest_polygons = []
|
||||
lowest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
@@ -321,7 +344,7 @@ class QtoCalculator:
|
||||
lowest_z = z
|
||||
return lowest_polygons
|
||||
|
||||
def get_highest_polygons(self, o):
|
||||
def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
|
||||
highest_polygons = []
|
||||
highest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
@@ -337,13 +360,13 @@ class QtoCalculator:
|
||||
highest_z = z
|
||||
return highest_polygons
|
||||
|
||||
def get_edge_key_distance(self, obj, edge_key):
|
||||
def get_edge_key_distance(self, obj: bpy.types.Object, edge_key: tuple[int, int]) -> float:
|
||||
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
|
||||
|
||||
def get_edge_distance(self, obj, edge):
|
||||
def get_edge_distance(self, obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float:
|
||||
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
|
||||
|
||||
def get_net_floor_area(self, obj):
|
||||
def get_net_floor_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_footprint_area(obj)
|
||||
@@ -359,7 +382,7 @@ class QtoCalculator:
|
||||
|
||||
return total_net_floor_area
|
||||
|
||||
def get_gross_ceiling_area(self, obj):
|
||||
def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_top_area(obj)
|
||||
@@ -367,15 +390,15 @@ class QtoCalculator:
|
||||
total_gross_ceiling_area = 0
|
||||
|
||||
for decomposition in decompositions:
|
||||
decomposition_type = decomposition.get_info()["type"]
|
||||
decomposition_predefined_type = decomposition.get_info()["PredefinedType"]
|
||||
if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING":
|
||||
decomposition_class = decomposition.is_a()
|
||||
decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition)
|
||||
if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING":
|
||||
decomposition_obj = tool.Ifc.get_object(decomposition)
|
||||
total_gross_ceiling_area += self.get_gross_footprint_area(decomposition_obj)
|
||||
|
||||
return total_gross_ceiling_area
|
||||
|
||||
def get_net_ceiling_area(self, obj):
|
||||
def get_net_ceiling_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_net_top_area(obj)
|
||||
@@ -383,19 +406,19 @@ class QtoCalculator:
|
||||
total_net_ceiling_area = 0
|
||||
|
||||
for decomposition in decompositions:
|
||||
decomposition_type = decomposition.get_info()["type"]
|
||||
decomposition_predefined_type = decomposition.get_info()["PredefinedType"]
|
||||
if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING":
|
||||
decomposition_class = decomposition.is_a()
|
||||
decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition)
|
||||
if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING":
|
||||
decomposition_obj = tool.Ifc.get_object(decomposition)
|
||||
total_net_ceiling_area += self.get_net_footprint_area(decomposition_obj)
|
||||
|
||||
if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn":
|
||||
if decomposition_class == "IfcWall" or decomposition_class == "IfcColumn":
|
||||
decomposition_obj = tool.Ifc.get_object(decomposition)
|
||||
total_net_ceiling_area -= self.get_net_roofprint_area(decomposition_obj)
|
||||
|
||||
return total_net_ceiling_area
|
||||
|
||||
def get_space_net_volume(self, obj):
|
||||
def get_space_net_volume(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_volume(obj)
|
||||
@@ -410,7 +433,7 @@ class QtoCalculator:
|
||||
|
||||
return total_space_net_volume
|
||||
|
||||
def get_net_footprint_area(self, o):
|
||||
def get_net_footprint_area(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the area of the footprint of the object, excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -421,7 +444,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_gross_footprint_area(self, o):
|
||||
def get_gross_footprint_area(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -437,7 +460,7 @@ class QtoCalculator:
|
||||
self.delete_mesh(mesh)
|
||||
return gross_footprint_area
|
||||
|
||||
def get_net_roofprint_area(self, o):
|
||||
def get_net_roofprint_area(self, o: bpy.types.Object) -> float:
|
||||
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
|
||||
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
|
||||
|
||||
@@ -449,7 +472,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_side_area(self, o):
|
||||
def get_side_area(self, o: bpy.types.Object) -> float:
|
||||
# There are a few dumb options for this, but this seems the dumbest
|
||||
# until I get more practical experience on what works best.
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
@@ -457,8 +480,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x * z, y * z)
|
||||
|
||||
def get_cross_section_area(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
def get_cross_section_area(self, obj: bpy.types.Object) -> float:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
@@ -474,7 +496,7 @@ class QtoCalculator:
|
||||
return area
|
||||
# TODO handle other types of sections, and then fall back to mesh parsing
|
||||
|
||||
def get_gross_surface_area(self, o, vg_index=None):
|
||||
def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
|
||||
if vg_index is None:
|
||||
if not self.has_openings(o):
|
||||
return self.get_net_surface_area(o)
|
||||
@@ -492,29 +514,29 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_net_surface_area(self, obj):
|
||||
def get_net_surface_area(self, obj: bpy.types.Object) -> float:
|
||||
return self.get_mesh_area(obj.data)
|
||||
|
||||
def get_mesh_area(self, mesh):
|
||||
def get_mesh_area(self, mesh: bpy.types.Mesh) -> float:
|
||||
area = 0
|
||||
for polygon in mesh.polygons:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def is_polygon_in_vg(self, polygon, vertices_in_vg):
|
||||
def is_polygon_in_vg(self, polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool:
|
||||
for v in polygon.vertices:
|
||||
if v not in vertices_in_vg:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_net_volume(self, o):
|
||||
def get_net_volume(self, o: bpy.types.Object) -> float:
|
||||
o_mesh = bmesh.new()
|
||||
o_mesh.from_mesh(o.data)
|
||||
volume = o_mesh.calc_volume()
|
||||
o_mesh.free()
|
||||
return volume
|
||||
|
||||
def get_gross_volume(self, o):
|
||||
def get_gross_volume(self, o: bpy.types.Object) -> float:
|
||||
if not self.has_openings(o):
|
||||
return self.get_net_volume(o)
|
||||
|
||||
@@ -529,16 +551,18 @@ class QtoCalculator:
|
||||
|
||||
return gross_volume
|
||||
|
||||
def has_openings(self, obj):
|
||||
def has_openings(
|
||||
self, obj: bpy.types.Object
|
||||
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
return element and getattr(element, "HasOpenings", [])
|
||||
|
||||
def get_obj_decompositions(self, obj):
|
||||
def get_obj_decompositions(self, obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
return decompositions
|
||||
|
||||
def get_gross_weight(self, obj):
|
||||
def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
obj_mass_density = self.get_obj_mass_density(obj)
|
||||
if not obj_mass_density:
|
||||
return
|
||||
@@ -546,7 +570,7 @@ class QtoCalculator:
|
||||
gross_weight = obj_mass_density * gross_volume
|
||||
return gross_weight
|
||||
|
||||
def get_net_weight(self, obj):
|
||||
def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
obj_mass_density = self.get_obj_mass_density(obj)
|
||||
if not obj_mass_density:
|
||||
return
|
||||
@@ -554,7 +578,7 @@ class QtoCalculator:
|
||||
net_weight = obj_mass_density * net_volume
|
||||
return net_weight
|
||||
|
||||
def get_obj_mass_density(self, obj):
|
||||
def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
material = ifcopenshell.util.element.get_material(entity)
|
||||
if material is None:
|
||||
@@ -629,7 +653,7 @@ class QtoCalculator:
|
||||
# volume += v1.dot(v2.cross(v3)) / 6.0
|
||||
# return volume
|
||||
|
||||
def get_opening_type(self, opening, obj):
|
||||
def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
|
||||
"""_summary_: Returns the opening type - OPENING / RECESS
|
||||
|
||||
:param blender-object opening: blender opening object
|
||||
@@ -649,14 +673,22 @@ class QtoCalculator:
|
||||
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
|
||||
|
||||
def get_opening_area(
|
||||
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
|
||||
):
|
||||
self,
|
||||
obj: bpy.types.Object,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
min_area: int = 0,
|
||||
ignore_recesses: bool = False,
|
||||
) -> float:
|
||||
"""_summary_: Returns the lateral area of the openings in the object.
|
||||
|
||||
:param obj: blender object
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
|
||||
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
|
||||
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area.
|
||||
Openings with a normal_vector lower than this value will be ignored, defaults to 45
|
||||
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area.
|
||||
Openings with a normal_vector greater than this value will be ignored,defaults to 135
|
||||
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored,
|
||||
defaults to 0
|
||||
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
|
||||
:return float: Opening Area
|
||||
"""
|
||||
@@ -702,14 +734,14 @@ class QtoCalculator:
|
||||
|
||||
def get_lateral_area(
|
||||
self,
|
||||
obj,
|
||||
obj: bpy.types.Object,
|
||||
subtract_openings: bool = True,
|
||||
exclude_end_areas: bool = False,
|
||||
exclude_side_areas: bool = False,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
main_axis: str = "",
|
||||
):
|
||||
) -> float:
|
||||
"""_summary_
|
||||
|
||||
:param blender-object obj: blender object, bpy.types.Object
|
||||
@@ -760,7 +792,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area + total_opening_area
|
||||
|
||||
def get_gross_side_area(self, obj):
|
||||
def get_gross_side_area(self, obj: bpy.types.Object) -> float:
|
||||
if not self.has_openings(obj):
|
||||
return self.get_net_side_area(obj)
|
||||
|
||||
@@ -768,15 +800,15 @@ class QtoCalculator:
|
||||
|
||||
return gross_side_area
|
||||
|
||||
def get_net_side_area(self, obj):
|
||||
def get_net_side_area(self, obj: bpy.types.Object) -> float:
|
||||
net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
|
||||
return net_side_area
|
||||
|
||||
def get_outer_surface_area(self, obj):
|
||||
def get_outer_surface_area(self, obj: bpy.types.Object) -> float:
|
||||
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
|
||||
return outer_surface_area
|
||||
|
||||
def get_end_area(self, obj):
|
||||
def get_end_area(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
gross_mesh = self.get_gross_element_mesh(element)
|
||||
gross_obj = bpy.data.objects.new("MyObject", gross_mesh)
|
||||
@@ -790,7 +822,7 @@ class QtoCalculator:
|
||||
|
||||
return end_area
|
||||
|
||||
def get_gross_top_area(self, obj, angle: int = 45):
|
||||
def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float:
|
||||
"""_summary_: Returns the gross top area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
@@ -827,12 +859,14 @@ class QtoCalculator:
|
||||
return area + opening_area
|
||||
|
||||
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
|
||||
def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True):
|
||||
def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
|
||||
"""_summary_: Returns the net top area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
|
||||
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
|
||||
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area.
|
||||
Values lower than this will be ignored, defaults to 45
|
||||
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills),
|
||||
defaults to True
|
||||
:return float: Net Top Area
|
||||
"""
|
||||
z_axis = (0, 0, 1)
|
||||
@@ -852,11 +886,11 @@ class QtoCalculator:
|
||||
|
||||
return area
|
||||
|
||||
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
|
||||
def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
|
||||
"""_summary_: Returns the projected area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
|
||||
:param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z"
|
||||
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
|
||||
:return float: Projected Area
|
||||
"""
|
||||
@@ -891,7 +925,7 @@ class QtoCalculator:
|
||||
return projected_polygon.area + void_area
|
||||
return projected_polygon.area
|
||||
|
||||
def get_OBB_object(self, obj):
|
||||
def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -932,7 +966,7 @@ class QtoCalculator:
|
||||
|
||||
return new_OBB_object
|
||||
|
||||
def get_AABB_object(self, obj):
|
||||
def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -988,12 +1022,12 @@ class QtoCalculator:
|
||||
|
||||
def get_bisected_obj(
|
||||
self,
|
||||
obj,
|
||||
plane_co_pos,
|
||||
plane_no_pos,
|
||||
plane_co_neg,
|
||||
plane_no_neg,
|
||||
):
|
||||
obj: bpy.types.Object,
|
||||
plane_co_pos: VectorTuple,
|
||||
plane_no_pos: VectorTuple,
|
||||
plane_co_neg: VectorTuple,
|
||||
plane_no_neg: VectorTuple,
|
||||
) -> bpy.types.Object:
|
||||
"""_summary_: Returns the object bisected by two planes.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1031,11 +1065,12 @@ class QtoCalculator:
|
||||
|
||||
return bis_obj
|
||||
|
||||
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
|
||||
def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
|
||||
"""_summary_: Returns the total contact area of the object with other objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:param list [] class_filter: A list of classes used to filter the objects
|
||||
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:return float: Total contact area of the object with other objects.
|
||||
"""
|
||||
total_contact_area = 0
|
||||
@@ -1046,11 +1081,12 @@ class QtoCalculator:
|
||||
|
||||
return total_contact_area
|
||||
|
||||
def get_touching_objects(self, obj, class_filter):
|
||||
def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
|
||||
"""_summary_: Returns a list of objects that are touching the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:param list [] class_filter: A list of classes used to filter the objects
|
||||
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:return list: List of touching objects
|
||||
"""
|
||||
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
|
||||
@@ -1094,7 +1130,7 @@ class QtoCalculator:
|
||||
|
||||
return touching_objects
|
||||
|
||||
def get_contact_area(self, object1, object2):
|
||||
def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the contact area between two objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1109,7 +1145,13 @@ class QtoCalculator:
|
||||
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
|
||||
return total_area
|
||||
|
||||
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
|
||||
def get_intersection_between_polygons(
|
||||
self,
|
||||
object1: bpy.types.Object,
|
||||
poly1: bpy.types.MeshPolygon,
|
||||
object2: bpy.types.Object,
|
||||
poly2: bpy.types.MeshPolygon,
|
||||
) -> float:
|
||||
"""_summary_: Returns the intersection between two polygons.
|
||||
|
||||
:param blender-object object1: Blender Object
|
||||
@@ -1152,7 +1194,9 @@ class QtoCalculator:
|
||||
# TopologicalError - Generated Geometry might be invalid
|
||||
return 0
|
||||
|
||||
def create_shapely_polygon(self, obj, polygon, trans_matrix):
|
||||
def create_shapely_polygon(
|
||||
self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
|
||||
) -> Polygon:
|
||||
"""_summary_: Create a shapely polygon
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1171,12 +1215,14 @@ class QtoCalculator:
|
||||
polygon_tuples.append((x, y))
|
||||
return Polygon(polygon_tuples)
|
||||
|
||||
def get_gross_element_mesh(self, element):
|
||||
def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
|
||||
return self.create_mesh_from_shape(element, settings)
|
||||
|
||||
def create_mesh_from_shape(self, element, settings=None):
|
||||
def create_mesh_from_shape(
|
||||
self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
|
||||
) -> bpy.types.Mesh:
|
||||
if settings is None:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||
@@ -1203,12 +1249,12 @@ class QtoCalculator:
|
||||
mesh.update()
|
||||
return mesh
|
||||
|
||||
def get_bmesh_from_mesh(self, mesh):
|
||||
def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
return bm
|
||||
|
||||
def get_object_main_axis(self, o):
|
||||
def get_object_main_axis(self, o: bpy.types.Object) -> AxisType:
|
||||
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
@@ -1227,18 +1273,18 @@ class QtoCalculator:
|
||||
else:
|
||||
return "x"
|
||||
|
||||
def is_opening_horizontal(self, o):
|
||||
def is_opening_horizontal(self, o: bpy.types.Object) -> bool:
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
return z < x and z < y
|
||||
|
||||
def delete_mesh(self, mesh):
|
||||
def delete_mesh(self, mesh: bpy.types.Mesh) -> None:
|
||||
mesh.user_clear()
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
def delete_obj(self, obj):
|
||||
def delete_obj(self, obj: bpy.types.Object) -> None:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ def get_active_pset_obj_name(context, obj_type):
|
||||
return ""
|
||||
|
||||
|
||||
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
|
||||
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True):
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
if "is_expanded" not in pset:
|
||||
@@ -114,16 +114,20 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
op = row.operator("bim.remove_pset", icon="X", text="")
|
||||
remove_pset_row = row.row(align=True)
|
||||
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
remove_pset_row.enabled = allow_removing
|
||||
elif props.active_pset_id != pset_id:
|
||||
row.label(text=pset["Name"], icon="COPY_ID")
|
||||
op = row.operator("bim.remove_pset", icon="X", text="")
|
||||
remove_pset_row = row.row(align=True)
|
||||
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
remove_pset_row.enabled = allow_removing
|
||||
if pset["is_expanded"]:
|
||||
if props.active_pset_id == pset_id:
|
||||
for prop in props.properties:
|
||||
@@ -204,14 +208,17 @@ class BIM_PT_object_psets(Panel):
|
||||
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
|
||||
|
||||
if ObjectPsetsData.data["psets"]:
|
||||
self.layout.label(text="Instance:")
|
||||
if ObjectPsetsData.data["is_occurrence"]:
|
||||
self.layout.label(text="Occurrence Properties:")
|
||||
else:
|
||||
self.layout.label(text="Type Properties:")
|
||||
for pset in ObjectPsetsData.data["psets"]:
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
|
||||
|
||||
if ObjectPsetsData.data["inherited_psets"]:
|
||||
self.layout.label(text="Type:")
|
||||
self.layout.label(text="Inherited Type Properties:", icon="CON_CHILDOF")
|
||||
for pset in ObjectPsetsData.data["inherited_psets"]:
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False)
|
||||
|
||||
|
||||
class BIM_PT_object_qtos(Panel):
|
||||
@@ -246,16 +253,19 @@ class BIM_PT_object_qtos(Panel):
|
||||
|
||||
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO":
|
||||
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
|
||||
|
||||
|
||||
if ObjectQtosData.data["qtos"]:
|
||||
self.layout.label(text="Instance:")
|
||||
if ObjectQtosData.data["is_occurrence"]:
|
||||
self.layout.label(text="Occurrence Quantities:")
|
||||
else:
|
||||
self.layout.label(text="Type Quantities:")
|
||||
for qto in ObjectQtosData.data["qtos"]:
|
||||
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
|
||||
|
||||
if ObjectQtosData.data["inherited_qsets"]:
|
||||
self.layout.label(text="Type:")
|
||||
self.layout.label(text="Inherited Type Quantities:", icon="CON_CHILDOF")
|
||||
for qset in ObjectQtosData.data["inherited_qsets"]:
|
||||
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object")
|
||||
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False)
|
||||
|
||||
|
||||
class BIM_PT_material_psets(Panel):
|
||||
|
||||
@@ -34,6 +34,7 @@ classes = (
|
||||
operator.RemovePropTemplate,
|
||||
operator.RemovePsetTemplate,
|
||||
operator.RemovePsetTemplateFile,
|
||||
operator.SavePsetTemplateFile,
|
||||
prop.PsetTemplate,
|
||||
prop.EnumerationValues,
|
||||
prop.PropTemplate,
|
||||
|
||||
@@ -42,6 +42,7 @@ class IfcClassData:
|
||||
cls.data["contexts"] = cls.contexts()
|
||||
cls.data["has_entity"] = cls.has_entity()
|
||||
cls.data["name"] = cls.name()
|
||||
cls.data["has_inherited_predefined_type"] = cls.has_inherited_predefined_type()
|
||||
cls.data["ifc_class"] = cls.ifc_class()
|
||||
cls.data["ifc_predefined_types"] = cls.ifc_predefined_types()
|
||||
cls.data["can_reassign_class"] = cls.can_reassign_class()
|
||||
@@ -162,6 +163,16 @@ class IfcClassData:
|
||||
name += f"[{predefined_type}]"
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def has_inherited_predefined_type(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
|
||||
if not element:
|
||||
return
|
||||
if element_type := ifcopenshell.util.element.get_type(element):
|
||||
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
|
||||
return ifcopenshell.util.element.get_predefined_type(element_type) not in ("NOTDEFINED", None)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def ifc_class(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
|
||||
@@ -174,6 +185,8 @@ class IfcClassData:
|
||||
if element:
|
||||
if element.is_a("IfcOpeningElement") or element.is_a("IfcOpeningStandardCase"):
|
||||
return False
|
||||
if element.is_a() in ("IfcWindowStyle", "IfcDoorStyle"): #see https://github.com/IfcOpenShell/IfcOpenShell/issues/4622#issuecomment-2095676368
|
||||
return True
|
||||
for product in cls.ifc_products():
|
||||
if element.is_a(product[0]):
|
||||
return True
|
||||
|
||||
@@ -215,7 +215,7 @@ class UnlinkObject(bpy.types.Operator):
|
||||
bl_idname = "bim.unlink_object"
|
||||
bl_label = "Unlink Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(name="Object Name")
|
||||
should_delete: bpy.props.BoolProperty(name="Delete IFC Element", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
@@ -227,6 +227,7 @@ class UnlinkObject(bpy.types.Operator):
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
|
||||
objects: list[bpy.types.Object]
|
||||
for obj in objects:
|
||||
was_active_object = obj == context.active_object
|
||||
|
||||
@@ -242,6 +243,27 @@ class UnlinkObject(bpy.types.Operator):
|
||||
if obj.data:
|
||||
obj_copy.data = obj.data.copy()
|
||||
|
||||
# prevent unlinking materials that might be used elsewhere
|
||||
replacements: dict[bpy.types.Material, bpy.types.Material] = dict()
|
||||
for material_slot in obj_copy.material_slots:
|
||||
material = material_slot.material
|
||||
if material is None:
|
||||
continue
|
||||
|
||||
if material in replacements:
|
||||
material_replacement = replacements[material]
|
||||
|
||||
# no need to copy non-ifc materials as unlinking won't do anything to them
|
||||
elif tool.Ifc.get_entity(material) is None and tool.Style.get_style(material) is None:
|
||||
replacements[material] = material
|
||||
continue
|
||||
|
||||
else:
|
||||
material_replacement = material.copy()
|
||||
replacements[material] = material_replacement
|
||||
|
||||
material_slot.material = material_replacement
|
||||
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
|
||||
obj = obj_copy
|
||||
|
||||
@@ -61,7 +61,10 @@ class BIM_PT_class(Panel):
|
||||
self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=IfcClassData.data["name"])
|
||||
row.label(
|
||||
text=IfcClassData.data["name"],
|
||||
icon="CON_CHILDOF" if IfcClassData.data["has_inherited_predefined_type"] else "NONE",
|
||||
)
|
||||
row.operator("bim.select_ifc_class", text="", icon="RESTRICT_SELECT_OFF")
|
||||
row.operator("bim.unlink_object", icon="UNLINKED", text="")
|
||||
if IfcClassData.data["can_reassign_class"]:
|
||||
|
||||
@@ -24,21 +24,15 @@ classes = (
|
||||
operator.ActivateIfcClassFilter,
|
||||
operator.AddFilter,
|
||||
operator.AddFilterGroup,
|
||||
operator.AddToIfcGroup,
|
||||
operator.ColourByProperty,
|
||||
operator.EditFilterQuery,
|
||||
operator.FilterModelElements,
|
||||
operator.IfcSelector,
|
||||
operator.LoadColourscheme,
|
||||
operator.LoadQuery,
|
||||
operator.LoadSearch,
|
||||
operator.OpenQueryLibrary,
|
||||
operator.RemoveFilter,
|
||||
operator.RemoveFilterGroup,
|
||||
operator.ResetObjectColours,
|
||||
operator.SaveColourscheme,
|
||||
operator.SaveSearch,
|
||||
operator.SaveSelectorQuery,
|
||||
operator.Search,
|
||||
operator.SelectByProperty,
|
||||
operator.SelectFilterElements,
|
||||
@@ -51,11 +45,6 @@ classes = (
|
||||
prop.BIMFilterClasses,
|
||||
prop.BIMFilterBuildingStoreys,
|
||||
prop.BIMSearchProperties,
|
||||
prop.SearchCollection,
|
||||
prop.SearchQueryFilter,
|
||||
prop.SearchQuery,
|
||||
prop.SearchQueryGroup,
|
||||
prop.IfcSelectorProperties,
|
||||
ui.BIM_PT_search,
|
||||
ui.BIM_PT_filter,
|
||||
ui.BIM_PT_colour_by_property,
|
||||
@@ -63,15 +52,12 @@ classes = (
|
||||
ui.BIM_UL_colourscheme,
|
||||
ui.BIM_UL_ifc_class_filter,
|
||||
ui.BIM_UL_ifc_building_storey_filter,
|
||||
ui.BIM_PT_IFCSelector,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
|
||||
bpy.types.Scene.IfcSelectorProperties = bpy.props.PointerProperty(type=prop.IfcSelectorProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMSearchProperties
|
||||
del bpy.types.Scene.IfcSelectorProperties
|
||||
|
||||
@@ -20,6 +20,7 @@ import re
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.guid
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.selector
|
||||
from ifcopenshell.util.selector import Selector
|
||||
@@ -235,7 +236,7 @@ class LoadSearch(Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
filter_groups = tool.Search.get_filter_groups(self.module)
|
||||
group = tool.Ifc.get().by_id(int(context.scene.BIMSearchProperties.saved_searches))
|
||||
query = tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
|
||||
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMSearchProperties
|
||||
@@ -271,25 +272,29 @@ class ColourByProperty(Operator):
|
||||
colourscheme = {}
|
||||
|
||||
if len(props.colourscheme):
|
||||
colourscheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
|
||||
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
|
||||
|
||||
for obj in context.visible_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
value = str(ifcopenshell.util.selector.get_element_value(element, query))
|
||||
if value not in colourscheme:
|
||||
colourscheme[value] = next(colours)[0:3]
|
||||
obj.color = (*colourscheme[value], 1)
|
||||
if value in colourscheme:
|
||||
colourscheme[value]["total"] += 1
|
||||
else:
|
||||
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
|
||||
obj.color = (*colourscheme[value]["colour"], 1)
|
||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
||||
if areas:
|
||||
areas[0].spaces[0].shading.color_type = "OBJECT"
|
||||
|
||||
props.colourscheme.clear()
|
||||
for value, colour in colourscheme.items():
|
||||
for value in sorted(colourscheme.keys()):
|
||||
data = colourscheme[value]
|
||||
new = props.colourscheme.add()
|
||||
new.name = str(value)
|
||||
new.colour = colour[0:3]
|
||||
new.total = data["total"]
|
||||
new.colour = data["colour"][0:3]
|
||||
return {"FINISHED"}
|
||||
|
||||
def store_state(self, context):
|
||||
@@ -351,16 +356,16 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
|
||||
query = props.colourscheme_query
|
||||
|
||||
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
|
||||
coulour_scheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
|
||||
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": cs.total} for cs in props.colourscheme}
|
||||
if group:
|
||||
group = group[0]
|
||||
description = json.loads(group.Description)
|
||||
description["colourscheme"] = coulour_scheme
|
||||
description["colourscheme"] = colourscheme
|
||||
description["colourscheme_query"] = query
|
||||
group.Description = json.dumps(description)
|
||||
else:
|
||||
description = json.dumps(
|
||||
{"type": "BBIM_Search", "colourscheme": coulour_scheme, "colourscheme_query": query}
|
||||
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
|
||||
)
|
||||
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
|
||||
|
||||
@@ -380,10 +385,11 @@ class LoadColourscheme(Operator, tool.Ifc.Operator):
|
||||
description = json.loads(group.Description)
|
||||
props.colourscheme_query = description.get("colourscheme_query")
|
||||
props.colourscheme.clear()
|
||||
for name, colour in description.get("colourscheme", {}).items():
|
||||
for name, data in description.get("colourscheme", {}).items():
|
||||
new = props.colourscheme.add()
|
||||
new.name = name
|
||||
new.colour = colour
|
||||
new.total = data["total"]
|
||||
new.colour = data["colour"]
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMSearchProperties
|
||||
@@ -623,223 +629,6 @@ class ShowAllElements(Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FilterModelElements(Operator):
|
||||
"""Filter model elements based on selection"""
|
||||
|
||||
bl_idname = "bim.filter_model_elements"
|
||||
bl_label = "Filter Model Elements"
|
||||
option: StringProperty("select|isolate|hide")
|
||||
|
||||
def execute(self, context):
|
||||
selection_props = context.scene.IfcSelectorProperties
|
||||
selection = (
|
||||
selection_props.selector_query_syntax
|
||||
if selection_props.manual_override
|
||||
else self.add_groups(selection_props)
|
||||
)
|
||||
selection_props.selector_query_syntax = selection
|
||||
r = core.search(tool.Search, tool.Spatial, query=selection, action=self.option)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
def add_groups(self, selector: str) -> str:
|
||||
selection = ""
|
||||
for group_index, group in enumerate(selector.groups):
|
||||
if group_index != 0:
|
||||
selection += " | "
|
||||
selection += "(" if len(selector.groups) > 1 else ""
|
||||
selection = self.add_queries(selection, group)
|
||||
|
||||
selection += ")" if len(selector.groups) > 1 else ""
|
||||
return selection
|
||||
|
||||
def add_queries(self, selection: str, group: str) -> str:
|
||||
for query_index, query in enumerate(group.queries):
|
||||
if query_index != 0:
|
||||
selection += " & " if query.and_or == "and" else " | "
|
||||
|
||||
if query.selector == "IFC Class":
|
||||
active_option = query.active_option.split(": ")[1]
|
||||
selection += f".{active_option}"
|
||||
selection = self.add_filters(selection, query)
|
||||
|
||||
elif query.selector == "GlobalId":
|
||||
selection += f"#{query.value}"
|
||||
|
||||
elif query.selector == "IfcElementType":
|
||||
index = int(query.active_sub_option.split(":")[0])
|
||||
selection += f"* #{query.sub_options[index].global_id}"
|
||||
|
||||
elif query.selector == "IfcSpatialElement":
|
||||
index = int(query.active_sub_option.split(":")[0])
|
||||
selection += f"@ #{query.sub_options[index].global_id}"
|
||||
return selection
|
||||
|
||||
def add_filters(self, selection: str, query: str) -> str:
|
||||
for f_index, f in enumerate(query.filters):
|
||||
if f_index != 0:
|
||||
selection += " & " if f.and_or == "and" else " | "
|
||||
selection += f".{query.active_option}"
|
||||
|
||||
if f.value in ("True", "False"):
|
||||
value = f.value
|
||||
elif f.value.isnumeric() and str(float(f.value))[0] == f.value[0]:
|
||||
value = f.value
|
||||
else:
|
||||
value = f'"{f.value}"'
|
||||
|
||||
selection += "["
|
||||
|
||||
if f.selector == "IfcPropertySet":
|
||||
selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}{f.comparison} {value}'
|
||||
elif f.selector == "Attribute":
|
||||
# we're using the prop_search functionality in blender which returns the index of the option. Sometimes the user can override this and enter a value that doesn't exist in the list. In this case there is no index and we need to handle it. @vulevukusej
|
||||
pattern = re.compile(r"^[0-9]+:")
|
||||
match = pattern.search(f.active_option)
|
||||
selection += f'{f.active_option.split(": ")[1] if match else f.active_option} {"!" if f.negation else ""}{f.comparison} {value}'
|
||||
selection += "]"
|
||||
return selection
|
||||
|
||||
|
||||
# This needs to be moved into ui code, I know ;) - vulevukusej
|
||||
class IfcSelector(Operator):
|
||||
"""Select elements in model with IFC Selector"""
|
||||
|
||||
bl_idname = "bim.ifc_selector"
|
||||
bl_label = "Select elements with IFC Selector"
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
from . import ui
|
||||
|
||||
ui.IfcSelectorUI.draw(context, self.layout)
|
||||
|
||||
|
||||
class SaveSelectorQuery(Operator):
|
||||
bl_idname = "bim.save_selector_query"
|
||||
bl_label = "Save Selector Query"
|
||||
save_name: StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "save_name")
|
||||
|
||||
def execute(self, context):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
new = ifc_selector.query_library.add()
|
||||
new.name = self.save_name
|
||||
new.query = ifc_selector.selector_query_syntax
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenQueryLibrary(Operator):
|
||||
"""Open Query Library"""
|
||||
|
||||
bl_idname = "bim.open_query_library"
|
||||
bl_label = "Open Query Library"
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
|
||||
for index, query in enumerate(ifc_selector.query_library):
|
||||
row = layout.row(align=True)
|
||||
row.prop(query, "query", text=query.name)
|
||||
op = row.operator("bim.load_query", icon="SORT_ASC", text="")
|
||||
op.index = index
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "query_library"
|
||||
op.index = index
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadQuery(Operator):
|
||||
bl_idname = "bim.load_query"
|
||||
bl_label = "Load Query"
|
||||
index: IntProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
close_operator_panel(event)
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddToIfcGroup(Operator):
|
||||
bl_idname = "bim.add_to_ifc_group"
|
||||
bl_label = "Add to IFC Group"
|
||||
group_name: StringProperty(name="Group Name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
bpy.ops.bim.load_groups()
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMGroupProperties
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_group")
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_groups",
|
||||
"",
|
||||
self.props,
|
||||
"groups",
|
||||
self.props,
|
||||
"active_group_index",
|
||||
)
|
||||
|
||||
if self.props.active_group_id:
|
||||
for attribute in self.props.group_attributes:
|
||||
if attribute.name in ["Name", "Description"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(attribute, "string_value", text=attribute.name)
|
||||
|
||||
def execute(self, context):
|
||||
active_group_index = self.props.active_group_index
|
||||
ifc_definition_id = self.props.groups[active_group_index].ifc_definition_id
|
||||
|
||||
bpy.ops.bim.enable_editing_group(group=ifc_definition_id)
|
||||
|
||||
selector_query_syntax = context.scene.IfcSelectorProperties.selector_query_syntax
|
||||
|
||||
for attribute in self.props.group_attributes:
|
||||
if attribute.name == "Description":
|
||||
if "*selector*" not in attribute.string_value:
|
||||
attribute.string_value += f" *selector*{selector_query_syntax}*selector*"
|
||||
else:
|
||||
new_description = attribute.string_value.split("*selector*")
|
||||
new_description[1] = selector_query_syntax
|
||||
attribute.string_value = "*selector*".join(new_description)
|
||||
|
||||
bpy.ops.bim.edit_group()
|
||||
bpy.ops.bim.disable_group_editing_ui()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectSimilar(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.select_similar"
|
||||
bl_label = "Select Similar"
|
||||
|
||||
@@ -78,7 +78,7 @@ def update_is_container_selected(self, context):
|
||||
else:
|
||||
for obj in context.selected_objects:
|
||||
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
|
||||
if (container and container.Name == self.name) or (not container and self.name== "None"):
|
||||
if (container and container.Name == self.name) or (not container and self.name == "None"):
|
||||
obj.select_set(False)
|
||||
new = self.unselected_objects.add()
|
||||
new.obj = obj
|
||||
@@ -100,6 +100,7 @@ class BIMFilterBuildingStoreys(PropertyGroup):
|
||||
|
||||
class BIMColour(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
total: IntProperty(name="Total")
|
||||
colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0)
|
||||
|
||||
|
||||
@@ -138,139 +139,3 @@ def get_classes(self, ifc_product):
|
||||
declarations = util.schema.get_subtypes(declaration)
|
||||
names = [d.name() for d in declarations]
|
||||
return [(c, c, "") for c in sorted(names)]
|
||||
|
||||
|
||||
def load_sub_options(self, context):
|
||||
if self.selector not in ["IfcClass"]:
|
||||
self.load_option = "sub_options"
|
||||
load_selection_options(self, context)
|
||||
|
||||
|
||||
def load_selection_options(self, context):
|
||||
ifc = IfcStore.file
|
||||
load_option = self.load_option
|
||||
op = getattr(self, load_option)
|
||||
op.clear()
|
||||
options = []
|
||||
|
||||
if load_option == "options":
|
||||
self.sub_options.clear()
|
||||
if self.selector == "IFC Class":
|
||||
options = get_classes(self, "IfcElement")
|
||||
options.append(("IfcSpace", "IfcSpace", ""))
|
||||
elif self.selector == "IfcSpatialElement":
|
||||
options = get_classes(self, "IfcSpatialElement")
|
||||
elif self.selector == "IfcElementType":
|
||||
options = get_classes(self, "IfcElementType")
|
||||
elif self.selector == "GlobalId":
|
||||
return
|
||||
elif self.selector == "IfcPropertySet":
|
||||
psets = Selector.parse(ifc, ".IfcPropertySet")
|
||||
options = set([o.Name for o in psets])
|
||||
elif self.selector == "Attribute":
|
||||
attributes = ["GlobalId", "Name", "Description", "ObjectType", "Tag", "PredefinedType"]
|
||||
for a in attributes:
|
||||
options.append((a, a, ""))
|
||||
|
||||
elif load_option == "sub_options":
|
||||
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
|
||||
active_option = self.active_option.split(": ")[1]
|
||||
options = Selector.parse(ifc, f".{active_option}")
|
||||
|
||||
elif self.selector == "IfcPropertySet":
|
||||
options = set()
|
||||
active_pset = self.active_option.split(": ")[1]
|
||||
psets_in_file = Selector.parse(ifc, f'.IfcPropertySet[Name="{active_pset}"]')
|
||||
for pset in psets_in_file:
|
||||
for prop in pset.HasProperties:
|
||||
options.add(prop.Name)
|
||||
|
||||
for index, option in enumerate(options):
|
||||
new = op.add()
|
||||
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
|
||||
if self.load_option == "sub_options":
|
||||
new.name = f"{index}: {option.Name}"
|
||||
new.global_id = option.GlobalId
|
||||
else:
|
||||
new.name = f"{index}: {option[0]}"
|
||||
elif self.selector == "IfcPropertySet":
|
||||
new.name = f"{index}: {option}"
|
||||
else:
|
||||
new.name = f"{index}: {option[0]}"
|
||||
self.load_option = "options"
|
||||
|
||||
|
||||
def load_spatial_elements(self, context):
|
||||
ifc = IfcStore.file
|
||||
col_items = Selector.parse(ifc, f".{self.selected_spatial_element}")
|
||||
collection = getattr(self, "sub_spatial_elements", None)
|
||||
collection.clear()
|
||||
for index, c in enumerate(col_items):
|
||||
new = collection.add()
|
||||
new.name = f"{index}-{c.Name}"
|
||||
|
||||
|
||||
class SearchCollection(PropertyGroup):
|
||||
name: StringProperty()
|
||||
long_name: StringProperty()
|
||||
global_id: StringProperty()
|
||||
query: StringProperty()
|
||||
|
||||
|
||||
class IfcSelector:
|
||||
and_or: EnumProperty(
|
||||
items=[(i, i, i) for i in ["and", "or"]],
|
||||
)
|
||||
negation: BoolProperty(name="not")
|
||||
comparison: EnumProperty(
|
||||
items=[
|
||||
("=", "equal to", ""),
|
||||
("*=", "contains", ""),
|
||||
(">=", "greater than or equal to", ""),
|
||||
("<=", "lesser than or equal to", ""),
|
||||
(">", "greater than", ""),
|
||||
("<", "less than", ""),
|
||||
],
|
||||
)
|
||||
load_option: StringProperty(
|
||||
default="options", description="controls whether or not options or sub_options are loaded"
|
||||
)
|
||||
options: CollectionProperty(type=SearchCollection)
|
||||
active_option: StringProperty(update=load_sub_options)
|
||||
sub_options: CollectionProperty(type=SearchCollection)
|
||||
active_sub_option: StringProperty()
|
||||
value: StringProperty(description="generic 'value' that can be used in multiple scenarios")
|
||||
|
||||
|
||||
class SearchQueryFilter(PropertyGroup, IfcSelector):
|
||||
selector: EnumProperty(
|
||||
items=[(i, i, i) for i in ["-", "IfcPropertySet", "Attribute"]],
|
||||
name="Filter selection by",
|
||||
update=load_selection_options,
|
||||
default="-",
|
||||
)
|
||||
|
||||
|
||||
class SearchQuery(PropertyGroup, IfcSelector):
|
||||
filters: CollectionProperty(type=SearchQueryFilter)
|
||||
selector: EnumProperty(
|
||||
items=[(i, i, i) for i in ["-", "IFC Class", "IfcSpatialElement", "IfcElementType", "GlobalId"]],
|
||||
name="Selector type",
|
||||
update=load_selection_options,
|
||||
default="-",
|
||||
)
|
||||
|
||||
|
||||
class SearchQueryGroup(PropertyGroup, IfcSelector):
|
||||
queries: CollectionProperty(type=SearchQuery)
|
||||
|
||||
|
||||
class IfcSelectorProperties(PropertyGroup, IfcSelector):
|
||||
groups: CollectionProperty(type=SearchQueryGroup)
|
||||
selector_query_syntax: StringProperty()
|
||||
|
||||
query_library: CollectionProperty(type=SearchCollection)
|
||||
active_query: StringProperty()
|
||||
|
||||
active_query: StringProperty()
|
||||
manual_override: BoolProperty(default=False, description="Toggle to allow manual typing of query-syntax")
|
||||
|
||||
@@ -125,7 +125,7 @@ class BIM_UL_colourscheme(bpy.types.UIList):
|
||||
if not item:
|
||||
return
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.name)
|
||||
row.label(text=f"{item.name} ({item.total})")
|
||||
row.prop(item, "colour", text="")
|
||||
|
||||
|
||||
@@ -159,153 +159,3 @@ class BIM_UL_ifc_building_storey_filter(bpy.types.UIList):
|
||||
split = split.column()
|
||||
split.scale_x = 0.5
|
||||
split.label(text=str(item.total))
|
||||
|
||||
|
||||
class BIM_PT_IFCSelector(Panel):
|
||||
bl_label = "Selector"
|
||||
bl_idname = "BIM_PT_ifc_selector"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_sandbox"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("bim.ifc_selector")
|
||||
|
||||
|
||||
# this doesn't inherit from Panel since it's just a class to abstract away UI code from the IfcSelector Operator
|
||||
class IfcSelectorUI:
|
||||
@classmethod
|
||||
def draw(self, context, layout):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
row = layout.row()
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", text="Add selection group")
|
||||
op.option = "add"
|
||||
op.collection = "groups"
|
||||
layout.separator()
|
||||
|
||||
self.draw_query_group_ui(self, ifc_selector, layout)
|
||||
|
||||
if len(ifc_selector.groups) != 0:
|
||||
row = layout.row(align=True)
|
||||
row.prop(ifc_selector, "selector_query_syntax", text="Query Syntax")
|
||||
row.prop(ifc_selector, "manual_override", text="")
|
||||
|
||||
select = row.operator("bim.filter_model_elements", text="", icon="RESTRICT_SELECT_OFF")
|
||||
select.option = "select"
|
||||
isolate = row.operator("bim.filter_model_elements", text="", icon="ZOOM_SELECTED")
|
||||
isolate.option = "isolate"
|
||||
hide = row.operator("bim.filter_model_elements", text="", icon="HIDE_ON")
|
||||
hide.option = "hide"
|
||||
unhide = row.operator("bim.filter_model_elements", text="", icon="HIDE_OFF")
|
||||
unhide.option = "unhide"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator("bim.save_selector_query", text="Save Query")
|
||||
op = row.operator("bim.open_query_library", text="Load Query")
|
||||
row.operator("bim.add_to_ifc_group", text="Add to IFC Group")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.show_scene_elements", text="Show All Elements", icon="HIDE_OFF")
|
||||
|
||||
def draw_query_group_ui(self, ifc_selector, layout):
|
||||
for index, group in enumerate(ifc_selector.groups):
|
||||
row = layout.row()
|
||||
row.alignment = "CENTER"
|
||||
row.label(text="or") if index != 0 else None
|
||||
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.label(text=f"Group #{str(index+1)}")
|
||||
row.context_pointer_set(name="bim_prop_group", data=group)
|
||||
op = row.operator("bim.edit_blender_collection", text="Add query", icon="PLUS")
|
||||
op.option = "add"
|
||||
op.collection = "queries"
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", text="Remove selection group")
|
||||
op.option = "remove"
|
||||
op.collection = "groups"
|
||||
op.index = index
|
||||
|
||||
self.draw_query_ui(self, group, box)
|
||||
|
||||
def draw_query_ui(self, group, box):
|
||||
for index, query in enumerate(group.queries):
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
|
||||
row.prop(query, "and_or", text="") if index != 0 else None
|
||||
if query.and_or == "or":
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.prop(query, "selector", text="")
|
||||
|
||||
if query.selector in ["IFC Class", "IfcSpatialElement", "IfcElementType"]:
|
||||
row.label(text="Equals")
|
||||
row.prop_search(query, "active_option", query, "options", text="")
|
||||
row.prop_search(query, "active_sub_option", query, "sub_options", text="") if len(
|
||||
query.sub_options
|
||||
) != 0 else None
|
||||
self.draw_filter_ui(self, box, query)
|
||||
|
||||
elif query.selector in ["GlobalId", "Attribute"]:
|
||||
row.prop(query, "negation", text="")
|
||||
row.prop(query, "comparison", text="")
|
||||
row.prop(query, "value", text="")
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=group)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "queries"
|
||||
op.index = index
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=query)
|
||||
op = (
|
||||
row.operator("bim.edit_blender_collection", text="Add filter")
|
||||
if query.selector == "IFC Class"
|
||||
else None
|
||||
)
|
||||
if op:
|
||||
op.option = "add"
|
||||
op.collection = "filters"
|
||||
|
||||
def draw_filter_ui(self, box, query):
|
||||
for filter_index, f in enumerate(query.filters):
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.label(text=" ↪")
|
||||
row.prop(f, "and_or", text="") if filter_index != 0 else None
|
||||
if f.and_or == "or":
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.label(text=" ↪")
|
||||
row.prop(f, "selector", text="")
|
||||
|
||||
if f.selector == "Attribute":
|
||||
row.prop_search(f, "active_option", f, "options", text="", results_are_suggestions=True)
|
||||
row.prop(
|
||||
f,
|
||||
"negation",
|
||||
)
|
||||
row.prop(f, "comparison", text="")
|
||||
row.prop(f, "value", text="")
|
||||
|
||||
elif f.selector == "IfcPropertySet":
|
||||
row.prop_search(f, "active_option", f, "options", text="")
|
||||
row.prop_search(f, "active_sub_option", f, "sub_options", text="")
|
||||
row.prop(f, "negation")
|
||||
row.prop(f, "comparison", text="")
|
||||
row.prop(f, "value", text="")
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=query)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "filters"
|
||||
op.index = filter_index
|
||||
|
||||
@@ -473,7 +473,7 @@ class AssignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.file,
|
||||
**{
|
||||
"relating_object": self.file.by_id(self.work_plan),
|
||||
"product": self.file.by_id(self.load_case),
|
||||
"products": [self.file.by_id(self.load_case)],
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
@@ -492,7 +492,7 @@ class UnassignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.file,
|
||||
**{
|
||||
"relating_object": self.file.by_id(self.work_plan),
|
||||
"product": self.file.by_id(self.load_case),
|
||||
"products": [self.file.by_id(self.load_case)],
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -30,6 +30,7 @@ classes = (
|
||||
operator.DisableAddingPresentationStyle,
|
||||
operator.DisableEditingStyle,
|
||||
operator.DisableEditingStyles,
|
||||
operator.DuplicateStyle,
|
||||
operator.EditStyle,
|
||||
operator.EditSurfaceStyle,
|
||||
operator.EnableAddingPresentationStyle,
|
||||
|
||||
@@ -23,6 +23,7 @@ import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.style as core
|
||||
import ifcopenshell.util.representation
|
||||
from blenderbim.bim.module.style.prop import switch_shading
|
||||
from pathlib import Path
|
||||
from mathutils import Vector
|
||||
|
||||
@@ -126,6 +127,10 @@ class DisableEditingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Style.reload_material_from_ifc(material)
|
||||
props.is_editing_style = 0
|
||||
|
||||
# restore selected style type
|
||||
material = tool.Ifc.get_object(style)
|
||||
material.BIMStyleProperties.active_style_type = material.BIMStyleProperties.active_style_type
|
||||
|
||||
|
||||
class EditStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_style"
|
||||
@@ -246,14 +251,15 @@ class BrowseExternalStyle(bpy.types.Operator):
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
external_style = None
|
||||
style_elements = None
|
||||
if self.active_surface_style_id:
|
||||
style = tool.Ifc.get().by_id(self.active_surface_style_id)
|
||||
external_style = tool.Style.get_style_elements(style).get("IfcExternallyDefinedSurfaceStyle", None)
|
||||
style_elements = tool.Style.get_style_elements(style)
|
||||
|
||||
# automatically select previously selected external style in file browser
|
||||
# if it exists in the file
|
||||
if external_style and self.filepath == "":
|
||||
if style_elements and self.filepath == "" and tool.Style.has_blender_external_style(style_elements):
|
||||
external_style = style_elements["IfcExternallyDefinedSurfaceStyle"]
|
||||
style_path = Path(tool.Ifc.resolve_uri(external_style.Location))
|
||||
self.directory = str(style_path.parent)
|
||||
self.filepath = str(style_path)
|
||||
@@ -310,6 +316,9 @@ class BrowseExternalStyle(bpy.types.Operator):
|
||||
attributes["Location"].string_value = filepath
|
||||
attributes["Identification"].string_value = f"{self.data_block_type}/{self.data_block}"
|
||||
attributes["Name"].string_value = self.data_block
|
||||
|
||||
style = tool.Ifc.get().by_id(self.active_surface_style_id)
|
||||
bpy.ops.bim.activate_external_style(material_name=tool.Ifc.get_object(style).name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -325,21 +334,35 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
material = context.active_object.active_material
|
||||
else:
|
||||
material = bpy.data.materials[self.material_name]
|
||||
external_style = tool.Style.get_style_elements(material)["IfcExternallyDefinedSurfaceStyle"]
|
||||
data_block_type, data_block = external_style.Identification.split("/")
|
||||
style_path = Path(tool.Ifc.resolve_uri(external_style.Location))
|
||||
|
||||
props = context.scene.BIMStylesProperties
|
||||
if props.is_editing:
|
||||
location = props.external_style_attributes["Location"].string_value
|
||||
identification = props.external_style_attributes["Identification"].string_value
|
||||
else:
|
||||
external_style = tool.Style.get_style_elements(material)["IfcExternallyDefinedSurfaceStyle"]
|
||||
location = external_style.Location
|
||||
identification = external_style.Identification
|
||||
|
||||
data_block_type, data_block = identification.split("/")
|
||||
style_path = Path(tool.Ifc.resolve_uri(location))
|
||||
|
||||
if style_path.suffix != ".blend":
|
||||
self.report({"ERROR"}, f"Error loading external style - only Blender external styles are supported")
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f'Error loading external style for "{material.name}" - only Blender external styles are supported',
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not style_path.exists():
|
||||
self.report({"ERROR"}, f"Error loading external style - cannot find file: '{style_path}'")
|
||||
self.report(
|
||||
{"ERROR"}, f"Error loading external style for \"{material.name}\" - cannot find file: '{style_path}'"
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
db = tool.Blender.append_data_block(str(style_path), data_block_type, data_block)
|
||||
if not db["data_block"]:
|
||||
self.report({"ERROR"}, f"Error loading external style - {db['msg']}")
|
||||
self.report({"ERROR"}, f"Error loading external style for \"{material.name}\" - {db['msg']}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.copy_material_attributes(db["data_block"], material)
|
||||
@@ -488,6 +511,22 @@ class EnableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_adding = True
|
||||
|
||||
|
||||
class DuplicateStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.duplicate_style"
|
||||
bl_label = "Duplicate Style"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
style: bpy.props.IntProperty(name="Style ID")
|
||||
|
||||
def _execute(self, context):
|
||||
style_type = context.scene.BIMStylesProperties.style_type
|
||||
ifc_file = tool.Ifc.get()
|
||||
style = ifc_file.by_id(self.style)
|
||||
tool.Style.duplicate_style(style)
|
||||
bpy.ops.bim.disable_editing_styles()
|
||||
bpy.ops.bim.load_styles(style_type=style_type)
|
||||
|
||||
|
||||
class DisableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_adding_presentation_style"
|
||||
bl_label = "Disable Add Presentation Style"
|
||||
@@ -566,7 +605,8 @@ class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_editing_class = self.ifc_class
|
||||
tool.Style.set_surface_style_props()
|
||||
|
||||
surface_style = tool.Style.get_style_elements(style).get(self.ifc_class, None)
|
||||
style_elements = tool.Style.get_style_elements(style)
|
||||
surface_style = style_elements.get(self.ifc_class, None)
|
||||
attributes = tool.Style.get_style_ui_props_attributes(self.ifc_class)
|
||||
|
||||
# lighting style require special handling since Attribute doesn't support colors
|
||||
@@ -586,6 +626,17 @@ class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
attributes.clear()
|
||||
blenderbim.bim.helper.import_attributes2(surface_style or self.ifc_class, attributes, callback)
|
||||
|
||||
material = tool.Ifc.get_object(style)
|
||||
active_style_type = material.BIMStyleProperties.active_style_type
|
||||
if self.ifc_class == "IfcExternallyDefinedSurfaceStyle" and active_style_type != "External":
|
||||
if tool.Style.has_blender_external_style(style_elements):
|
||||
switch_shading(material, "External")
|
||||
elif (
|
||||
self.ifc_class in ("IfcSurfaceStyleShading", "IfcSurfaceStyleRendering", "IfcSurfaceStyleWithTextures")
|
||||
and active_style_type != "Shading"
|
||||
):
|
||||
switch_shading(material, "Shading")
|
||||
|
||||
|
||||
class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_surface_style"
|
||||
@@ -610,6 +661,10 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.props.is_editing_style = 0
|
||||
core.load_styles(tool.Style, style_type=self.props.style_type)
|
||||
|
||||
# restore selected style type
|
||||
material = tool.Ifc.get_object(self.style)
|
||||
material.BIMStyleProperties.active_style_type = material.BIMStyleProperties.active_style_type
|
||||
|
||||
def edit_existing_style(self):
|
||||
material = tool.Ifc.get_object(self.style)
|
||||
if self.surface_style.is_a() == "IfcSurfaceStyleShading":
|
||||
|
||||
@@ -33,6 +33,8 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
import gettext
|
||||
from typing import Literal
|
||||
|
||||
|
||||
_ = gettext.gettext
|
||||
|
||||
@@ -251,19 +253,15 @@ class BIMStylesProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
|
||||
def update_shading_style(self, context):
|
||||
blender_material = self.id_data
|
||||
style_elements = tool.Style.get_style_elements(blender_material)
|
||||
if self.active_style_type == "External":
|
||||
if tool.Style.has_blender_external_style(style_elements):
|
||||
try:
|
||||
bpy.ops.bim.activate_external_style(material_name=blender_material.name)
|
||||
except RuntimeError as error:
|
||||
if str(error).startswith("Error: Error loading external style - "):
|
||||
return
|
||||
raise error
|
||||
|
||||
elif self.active_style_type == "Shading":
|
||||
def switch_shading(blender_material: bpy.types.Material, style_type: Literal["External", "Shading"]) -> None:
|
||||
if style_type == "External":
|
||||
try:
|
||||
bpy.ops.bim.activate_external_style(material_name=blender_material.name)
|
||||
except RuntimeError as error:
|
||||
if str(error).startswith("Error: Error loading external style for "):
|
||||
return
|
||||
raise error
|
||||
elif style_type == "Shading":
|
||||
style_elements = tool.Style.get_style_elements(blender_material)
|
||||
rendering_style = None
|
||||
texture_style = None
|
||||
@@ -279,6 +277,16 @@ def update_shading_style(self, context):
|
||||
|
||||
if rendering_style and texture_style:
|
||||
tool.Loader.create_surface_style_with_textures(blender_material, rendering_style, texture_style)
|
||||
|
||||
|
||||
def update_shading_style(self, context):
|
||||
blender_material = self.id_data
|
||||
style_elements = tool.Style.get_style_elements(blender_material)
|
||||
if self.active_style_type == "External":
|
||||
if tool.Style.has_blender_external_style(style_elements):
|
||||
switch_shading(blender_material, self.active_style_type)
|
||||
elif self.active_style_type == "Shading":
|
||||
switch_shading(blender_material, self.active_style_type)
|
||||
tool.Style.record_shading(blender_material)
|
||||
|
||||
|
||||
|
||||
@@ -44,19 +44,33 @@ class BIM_PT_styles(Panel):
|
||||
|
||||
self.props = context.scene.BIMStylesProperties
|
||||
|
||||
if self.props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
|
||||
if not self.props.is_adding:
|
||||
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
|
||||
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
|
||||
else:
|
||||
if not self.props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Styles".format(StylesData.data["total_styles"]), icon="SHADING_RENDERED")
|
||||
blenderbim.bim.helper.prop_with_search(row, self.props, "style_type", text="")
|
||||
row.operator("bim.load_styles", text="", icon="IMPORT").style_type = self.props.style_type
|
||||
return
|
||||
|
||||
active_style = self.props.styles and self.props.active_style_index < len(self.props.styles)
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
|
||||
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
if not self.props.is_adding:
|
||||
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
|
||||
if active_style:
|
||||
style = self.props.styles[self.props.active_style_index]
|
||||
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
|
||||
material = bpy.data.materials[material_name]
|
||||
|
||||
row.operator("bim.duplicate_style", text="", icon="DUPLICATE").style = style.ifc_definition_id
|
||||
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
|
||||
op = row.operator("bim.enable_editing_style", text="", icon="GREASEPENCIL")
|
||||
op.style = style.ifc_definition_id
|
||||
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
|
||||
|
||||
self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index")
|
||||
|
||||
# adding a new IfcSurfaceStyle
|
||||
@@ -77,17 +91,7 @@ class BIM_PT_styles(Panel):
|
||||
row.operator("bim.disable_adding_presentation_style", text="", icon="CANCEL")
|
||||
|
||||
# style ui tools
|
||||
if self.props.styles and self.props.active_style_index < len(self.props.styles):
|
||||
row = self.layout.row(align=True)
|
||||
style = self.props.styles[self.props.active_style_index]
|
||||
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
|
||||
material = bpy.data.materials[material_name]
|
||||
|
||||
op = row.operator("bim.enable_editing_style", text="Edit Style", icon="GREASEPENCIL")
|
||||
op.style = style.ifc_definition_id
|
||||
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
|
||||
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
|
||||
|
||||
if active_style:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="")
|
||||
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import bmesh
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.api
|
||||
import blenderbim.tool as tool
|
||||
@@ -77,7 +78,7 @@ class UnassignType(bpy.types.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or element.is_a("IfcElementType"):
|
||||
continue
|
||||
ifcopenshell.api.run("type.unassign_type", self.file, related_object=element)
|
||||
ifcopenshell.api.run("type.unassign_type", self.file, related_objects=[element])
|
||||
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
active_context = active_representation.ContextOfItems
|
||||
@@ -145,23 +146,51 @@ class SelectType(bpy.types.Operator):
|
||||
relating_type: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
element = tool.Ifc.get().by_id(self.relating_type)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
try:
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
except:
|
||||
self.report({"INFO"}, "Type object is hidden.")
|
||||
# IfcTypeProducts are only used for annotations and not part of the model interface.
|
||||
if element.is_a() != "IfcTypeProduct":
|
||||
try:
|
||||
context.scene.BIMModelProperties.ifc_class = element.is_a()
|
||||
context.scene.BIMModelProperties.relating_type_id = str(self.relating_type)
|
||||
except:
|
||||
# Potentially our BIM Tool is filtered to a specific element.
|
||||
pass
|
||||
|
||||
if self.relating_type: #if operator button sends a relating_type, the iterator only selects this one type
|
||||
element = tool.Ifc.get().by_id(self.relating_type)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
selected_objs = [obj]
|
||||
else: #else, the iterator selects all the types of all the selected objects
|
||||
selected_objs = context.selected_objects
|
||||
active_obj = context.active_object
|
||||
selected_objs.append(active_obj) #update selected_objs so the active_obj is at the end of the list
|
||||
|
||||
last_relating_type_obj = None
|
||||
types_collection_in_view_layer = self.find_collection_in_ifcproject(context, collection_name = "Types")
|
||||
types_collection_in_view_layer.hide_viewport = False
|
||||
types_collection = bpy.data.collections.get("Types")
|
||||
for type_obj in types_collection.objects:
|
||||
type_obj.hide_set(True)
|
||||
for obj in selected_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
relating_type = ifcopenshell.util.element.get_type(element)
|
||||
if relating_type:
|
||||
relating_type_obj = tool.Ifc.get_object(relating_type)
|
||||
if relating_type_obj:
|
||||
if relating_type_obj.hide_get():
|
||||
relating_type_obj.hide_set(False)
|
||||
relating_type_obj.select_set(True)
|
||||
last_relating_type_obj = relating_type_obj
|
||||
if not element.is_a("IfcTypeObject"):
|
||||
obj.select_set(False)
|
||||
|
||||
context.view_layer.objects.active = last_relating_type_obj #makes the active_obj's type the active object
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def find_collection_in_ifcproject(self, context, collection_name):
|
||||
|
||||
ifc_project_collection = None
|
||||
for child in context.view_layer.layer_collection.children:
|
||||
if "IfcProject" in child.name:
|
||||
ifc_project_collection = child
|
||||
break
|
||||
|
||||
if ifc_project_collection:
|
||||
collection_in_view_layer = ifc_project_collection.children.get(collection_name)
|
||||
return collection_in_view_layer
|
||||
|
||||
|
||||
class SelectSimilarType(bpy.types.Operator):
|
||||
bl_idname = "bim.select_similar_type"
|
||||
@@ -282,7 +311,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
material = self.add_default_material()
|
||||
rel = ifcopenshell.api.run(
|
||||
"material.assign_material", ifc_file, product=element, type="IfcMaterialLayerSet"
|
||||
"material.assign_material", ifc_file, products=[element], type="IfcMaterialLayerSet"
|
||||
)
|
||||
layer_set = rel.RelatingMaterial
|
||||
layer = ifcopenshell.api.run("material.add_layer", ifc_file, layer_set=layer_set, material=material)
|
||||
@@ -360,7 +389,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
rel = ifcopenshell.api.run(
|
||||
"material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet"
|
||||
"material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet"
|
||||
)
|
||||
profile_set = rel.RelatingMaterial
|
||||
material_profile = ifcopenshell.api.run(
|
||||
|
||||
@@ -88,7 +88,7 @@ class BIM_PT_type(Panel):
|
||||
if TypeData.data["relating_type"]:
|
||||
row.label(text=TypeData.data["relating_type"]["name"])
|
||||
op = row.operator("bim.select_type", icon="OBJECT_DATA", text="")
|
||||
op.relating_type = TypeData.data["relating_type"]["id"]
|
||||
op.relating_type = 0 #will only select the relating types of only the selected objects
|
||||
row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="")
|
||||
row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.unassign_type", icon="X", text="")
|
||||
|
||||
@@ -90,6 +90,15 @@ class OpenUri(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CloseError(bpy.types.Operator):
|
||||
bl_idname = "bim.close_error"
|
||||
bl_label = "Close Error"
|
||||
|
||||
def execute(self, context):
|
||||
blenderbim.last_error = None
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectURIAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.select_uri_attribute"
|
||||
bl_label = "Select URI Attribute"
|
||||
@@ -357,7 +366,7 @@ class OpenUpstream(bpy.types.Operator):
|
||||
if self.page == "home":
|
||||
webbrowser.open("https://blenderbim.org/")
|
||||
elif self.page == "docs":
|
||||
webbrowser.open("https://blenderbim.org/docs/")
|
||||
webbrowser.open("https://docs.blenderbim.org/")
|
||||
elif self.page == "wiki":
|
||||
webbrowser.open("https://wiki.osarch.org/index.php?title=Category:BlenderBIM_Add-on")
|
||||
elif self.page == "community":
|
||||
@@ -936,6 +945,31 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
|
||||
)
|
||||
|
||||
|
||||
class BIM_OT_select_object(bpy.types.Operator):
|
||||
bl_idname = "bim.select_object"
|
||||
bl_label = "Select Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj_name: bpy.props.StringProperty(description="Object Name To Select")
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.data.objects[self.obj_name]
|
||||
tool.Blender.set_objects_selection(context, obj, [obj], clear_previous_selection=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_delete_object(bpy.types.Operator):
|
||||
bl_idname = "bim.delete_object"
|
||||
bl_label = "Delete Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj_name: bpy.props.StringProperty(description="Object Name To Delete")
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.data.objects[self.obj_name]
|
||||
with context.temp_override(selected_objects=[obj], active_object=obj):
|
||||
bpy.ops.bim.override_object_delete(is_batch=False)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditBlenderCollection(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_blender_collection"
|
||||
bl_label = "Add or Remove Blender Collection Item"
|
||||
|
||||
@@ -273,6 +273,8 @@ class Attribute(PropertyGroup):
|
||||
def get_value(self):
|
||||
if self.is_optional and self.is_null:
|
||||
return None
|
||||
if self.data_type == "string":
|
||||
return self.string_value.replace("\\n", "\n")
|
||||
return getattr(self, str(self.get_value_name()), None)
|
||||
|
||||
def get_value_default(self):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user