mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Compare commits
368 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02a5b8d997 | |||
| f1b5f7d558 | |||
| 742a9ecd81 | |||
| 77a497cdf7 | |||
| 00576ef574 | |||
| c26caaa58d | |||
| b260bd3877 | |||
| cd6246c382 | |||
| a96d6dacc7 | |||
| 64c711a02a | |||
| d5e2fd5d3c | |||
| bb445663f8 | |||
| 1f7db75572 | |||
| f478ec8d6b | |||
| cf3ba334b7 | |||
| c36af110c4 | |||
| 91eac7085b | |||
| 239d1d6fd2 | |||
| 831709d514 | |||
| 5925476ed9 | |||
| 3edb3c8df3 | |||
| 72badd8427 | |||
| 7201df8d22 | |||
| 7b65108a76 | |||
| 0fbe4d80c7 | |||
| 54c689d615 | |||
| ab828b1a8b | |||
| 8ddd349f8b | |||
| 3221219f4a | |||
| 36387f348e | |||
| c39c6d81e3 | |||
| e8042de1ed | |||
| 78fd999c53 | |||
| 91d1cb79c2 | |||
| 832618b84a | |||
| e4ce53d548 | |||
| 7b92b5c7e2 | |||
| c8c08439b2 | |||
| 3476ee8fd5 | |||
| d2f331c813 | |||
| 9e9b294f44 | |||
| f824ee8ca4 | |||
| 919f673108 | |||
| b034b40b09 | |||
| 34f038530f | |||
| 1c70990b06 | |||
| 07eecad433 | |||
| ef87f0fe74 | |||
| 23c2d73e8e | |||
| 0b47fb5987 | |||
| 8a1dee532b | |||
| f0c6c4ca59 | |||
| 92cf0d9265 | |||
| 7c438b6b9b | |||
| 46454c5c94 | |||
| 73aeafb063 | |||
| e06297940f | |||
| 1652629332 | |||
| 3d8de40407 | |||
| 308457300e | |||
| 6ef96841ac | |||
| 289b2d1e9d | |||
| 96db699ad2 | |||
| 6b7eec0db6 | |||
| 80f35d0bda | |||
| df390a655d | |||
| 593fca7c3d | |||
| 9335c8a0c8 | |||
| eccc21277d | |||
| 2ce7e16537 | |||
| 49835ee303 | |||
| f5500da0af | |||
| c9552fac29 | |||
| b11b3b7fa8 | |||
| 8aecee5836 | |||
| 2f3b3011ec | |||
| 491c9203b0 | |||
| 7f2b996d23 | |||
| d2ea6f6656 | |||
| 25128e8aa8 | |||
| 30fa917e69 | |||
| 40e494996d | |||
| cfdc54064e | |||
| 6daead6b61 | |||
| 241d52f0ad | |||
| 68dd93da1d | |||
| 82703670f0 | |||
| cb8462a0d0 | |||
| 000245c26c | |||
| d97ee8eed4 | |||
| f8af28f12a | |||
| f6fab30d0a | |||
| f1a4ed702b | |||
| c4080626df | |||
| a8078e38d2 | |||
| c52fe8abf9 | |||
| b310b5aaba | |||
| 7246e75085 | |||
| 48c0378bff | |||
| 91c90e7b86 | |||
| 72abdb0569 | |||
| 7947ba4aaf | |||
| 2520a93055 | |||
| fa480d99a2 | |||
| 72c057e7a0 | |||
| 61cfb361fa | |||
| 3786cfa666 | |||
| cabc06821f | |||
| f9fef4f5f6 | |||
| 7c89d072e2 | |||
| dbb9e8c907 | |||
| 2df5b4c70e | |||
| a5aa608240 | |||
| f7454366cc | |||
| f773480917 | |||
| 89f9cd0403 | |||
| b21159c6b3 | |||
| 227f556669 | |||
| 42bd69929c | |||
| a3532b24b3 | |||
| 5486c4c8e9 | |||
| 0259d4c0cd | |||
| 66c4c61dc2 | |||
| 0f32ffe232 | |||
| 72630de77a | |||
| 9395bed72b | |||
| 507a5cab83 | |||
| a46a60996d | |||
| 3106168889 | |||
| 7e0c0c5c83 | |||
| 19205c8774 | |||
| 62b5f1d728 | |||
| 299df62758 | |||
| e58bfde500 | |||
| b60306cdbd | |||
| 61e0549e65 | |||
| 52ae759218 | |||
| 9b41614eff | |||
| 58e01751c1 | |||
| 624134d033 | |||
| 22f83b68e4 | |||
| 1b35ea122e | |||
| f1deefbcb3 | |||
| d6c020b5ee | |||
| ff4eddf38a | |||
| 2b34dc4ce7 | |||
| 530260ebe7 | |||
| 594268c860 | |||
| e5fa8a01b4 | |||
| ad934cd9a6 | |||
| 92fe325816 | |||
| 818bb9c823 | |||
| d71fad988d | |||
| ff0d6c810f | |||
| 8b72929d22 | |||
| 6452872e83 | |||
| 88d2c61453 | |||
| f777be6403 | |||
| 9aad249586 | |||
| 7168f8eeff | |||
| da306c6141 | |||
| e53c0b74b1 | |||
| cb2f58ba0f | |||
| c0c7727f36 | |||
| 60526e62b4 | |||
| 496e58418a | |||
| b976ad3880 | |||
| 31a763fe12 | |||
| 93ef2d0349 | |||
| 7828508179 | |||
| 7aa6be8e8a | |||
| 0679956add | |||
| b86f924543 | |||
| 962b8fac67 | |||
| fdcb705551 | |||
| b80ca3c167 | |||
| e8bd272189 | |||
| 25bd94e84a | |||
| 5face7170f | |||
| 434a11a502 | |||
| eb91f5179b | |||
| c6643da4ba | |||
| 18c93a13ec | |||
| 7cb582de5d | |||
| a5bb458d3d | |||
| c7f6a5e621 | |||
| 141e0240d4 | |||
| 15cd2b6075 | |||
| fab8b5b702 | |||
| 844f4c4944 | |||
| 03809b3d5e | |||
| 3e268be5c7 | |||
| 6e932ec2fb | |||
| 58414777be | |||
| 14f1acadb7 | |||
| f215670381 | |||
| 11df5065e4 | |||
| ed47b6b227 | |||
| 6216ee9a98 | |||
| 848d6b294e | |||
| 795a372ac9 | |||
| 03a6ee9121 | |||
| 5299a9d8a2 | |||
| a5d5d72064 | |||
| b6f2a76385 | |||
| ea6c8fe2f8 | |||
| c7268b6f42 | |||
| 5937ed4b63 | |||
| 8f650c56e5 | |||
| 1d972a6a6f | |||
| 70a1d7d345 | |||
| 519d59effe | |||
| 7f5f67b131 | |||
| 8c64d9ddcc | |||
| 397f333db0 | |||
| b01bd8afb3 | |||
| 1dd8624825 | |||
| bc0c2e641b | |||
| f5f68ea150 | |||
| 76ce117b46 | |||
| 52cad9d11c | |||
| 58af81e256 | |||
| fc2da4ee8f | |||
| c04fa12051 | |||
| 623d2b5be1 | |||
| 46319634fb | |||
| 91d2e5ccd1 | |||
| d23e32c64a | |||
| 764efa1fed | |||
| b051adcc6c | |||
| b815719477 | |||
| 0612d2b615 | |||
| 0da210b732 | |||
| 9429e478fe | |||
| 2f6c50d9c2 | |||
| 640341715c | |||
| 42878e325e | |||
| ac5cf95f9e | |||
| c9189055c7 | |||
| 6a08993f5e | |||
| df2a50b335 | |||
| c7543ff48d | |||
| 1f75fa09ae | |||
| 94ce8e2c9c | |||
| 85e94d2cfc | |||
| c2b332da48 | |||
| 6ee4edd0a0 | |||
| 0d757027e3 | |||
| 9ffe56e64d | |||
| 37a9a1b5d9 | |||
| c30c9d50c9 | |||
| 8e64b55290 | |||
| b45001a95a | |||
| fd65009f5a | |||
| 52036e9bd4 | |||
| 5399767898 | |||
| f96d477d2b | |||
| d5f3991d06 | |||
| 2f2e81f585 | |||
| 0b7442b288 | |||
| 229c7285c2 | |||
| 4fb99d2eed | |||
| 123557939f | |||
| 3c396d3180 | |||
| 5d39603308 | |||
| b792673f88 | |||
| cf65612a96 | |||
| 50bbe849ff | |||
| dc438b2560 | |||
| 6750dd927c | |||
| 9fb1986cad | |||
| 60f8d30fc1 | |||
| 67be2e8ce8 | |||
| 2557580500 | |||
| c0d2ac6ff9 | |||
| 52c168051e | |||
| e072da8d65 | |||
| 8a63275db0 | |||
| 69be3a005c | |||
| ad9ad3f060 | |||
| f0e5304b8e | |||
| c6454803ee | |||
| e8aa8c44e5 | |||
| 48b237ab54 | |||
| 608b3e5401 | |||
| c5d38227fc | |||
| e98ff136c2 | |||
| 02ebc80525 | |||
| 66431158b5 | |||
| dd044c8516 | |||
| 55763ea7df | |||
| 8fe92e810f | |||
| c26988b292 | |||
| 46e70c92c5 | |||
| 325c3665d9 | |||
| 9a80dc14e9 | |||
| 6bda066e1b | |||
| 912b01587b | |||
| 5d4d953320 | |||
| 11ed8cc787 | |||
| d1a7b99eda | |||
| 85bdeaeef4 | |||
| a5f0de4d19 | |||
| cb3392ec70 | |||
| 1dbe42061d | |||
| a024d5c07f | |||
| ea5e3c30b6 | |||
| ebfc8ef619 | |||
| c0d8467154 | |||
| d0adbab9c2 | |||
| 8e80f456cb | |||
| 61bd98e7db | |||
| 4bf0d9ccff | |||
| 1e36ac8740 | |||
| af864e8556 | |||
| 8cd9fb9015 | |||
| d9fdc0908a | |||
| a218ce5160 | |||
| b3a6b8e07f | |||
| 52700452d9 | |||
| aab011f7bf | |||
| 5f129e2357 | |||
| 4e76f36af1 | |||
| 6f3ff51d91 | |||
| 0de0422c2e | |||
| 98de86e0ba | |||
| 142e41b3ad | |||
| 1e0dc7749d | |||
| 0f29de0391 | |||
| 9933cc1b20 | |||
| fa6192e9bc | |||
| bfd4559da8 | |||
| 01f52e069e | |||
| 2fbc9e4424 | |||
| ba97da20eb | |||
| 44f5be142f | |||
| 2f951d352b | |||
| 8b23c615c3 | |||
| 7c694e5b3e | |||
| 8eeb212003 | |||
| 7bf8d58cde | |||
| b6e24559e5 | |||
| 0d87d5927e | |||
| dfd5cc4728 | |||
| 20b7ee009d | |||
| 19a3f56941 | |||
| 3b298d44c1 | |||
| 038d3bc41c | |||
| 235e89ae14 | |||
| e2de9efc33 | |||
| 8126085037 | |||
| bbe14834e4 | |||
| 45f2d301b3 | |||
| 2ecd11d71f | |||
| b42f072bd5 | |||
| 3b3668eedf | |||
| ef72cd3250 | |||
| 4f1e59a613 | |||
| 5960e4153c | |||
| 66f735fde9 | |||
| 1a3aa7f031 | |||
| ac41a9e63a | |||
| 37ab9c80e3 | |||
| 86a234d17e | |||
| f68baee5c3 | |||
| 0028fd3e9c | |||
| f5f9862b78 | |||
| 4e833e26ca |
@@ -39,7 +39,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310]
|
||||
pyver: [py39, py310, py311]
|
||||
config:
|
||||
- {
|
||||
name: "Windows Build",
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
- 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.10'
|
||||
python-version: '3.11'
|
||||
- run: echo ${{ env.DATE }}
|
||||
- name: Get current date
|
||||
id: date
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
name: ci-ifcopenshell-conda-daily
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/ci-ifcopenshell-conda-daily.yml
|
||||
- conda/**
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
@@ -15,17 +12,8 @@ on:
|
||||
- cron: "49 23 * * *" # 11min before utc midnight every day
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
test:
|
||||
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
|
||||
needs: activate
|
||||
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}-${{ matrix.variant }}
|
||||
runs-on: ${{ matrix.platform.distver }}
|
||||
defaults:
|
||||
run:
|
||||
@@ -34,18 +22,23 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [
|
||||
{ name: py310, distver: '3.10'},
|
||||
{ name: py311, distver: '3.11'}
|
||||
{ name: py311, distver: '3.11'},
|
||||
{ name: py312, distver: '3.12'}
|
||||
]
|
||||
platform: [
|
||||
{ name: Windows, distver: windows-2022, upload: 'true' },
|
||||
{ name: Linux, distver: ubuntu-22.04, upload: 'true' },
|
||||
{ name: macOS, distver: macos-12, upload: 'true' }
|
||||
]
|
||||
variant: [
|
||||
'novtk',
|
||||
# 'all'
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download MacOSX SDK
|
||||
if: ${{ matrix.platform.name == 'macOS' }}
|
||||
run: |
|
||||
@@ -53,21 +46,22 @@ jobs:
|
||||
tar xf MacOSX10.13.sdk.tar.xz && \
|
||||
sudo mv -v MacOSX10.13.sdk /opt/ && \
|
||||
ls /opt/
|
||||
- uses: seanmiddleditch/gha-setup-ninja@master
|
||||
- uses: mamba-org/provision-with-micromamba@main # https://github.com/mamba-org/provision-with-micromamba
|
||||
|
||||
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
channels: conda-forge
|
||||
channel-priority: strict
|
||||
environment-file: conda/environment.yml
|
||||
extra-specs: |
|
||||
python=${{ matrix.pyver.distver }}
|
||||
environment-name: build-env
|
||||
cache-environment: true
|
||||
condarc: |
|
||||
channels:
|
||||
- conda-forge
|
||||
channel_priority: strict
|
||||
create-args: >-
|
||||
python=3.11
|
||||
anaconda-client
|
||||
boa
|
||||
|
||||
- name: build, test and upload ifcopenshell
|
||||
if: ${{ matrix.platform.upload == 'true' }}
|
||||
run: |
|
||||
conda-build . --python ${{ matrix.pyver.distver }} -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user ifcopenshell
|
||||
working-directory: ./conda
|
||||
- name: build & test ifcopenshell
|
||||
if: ${{ matrix.platform.upload == 'false' }}
|
||||
run: |
|
||||
conda-build . --python ${{ matrix.pyver.distver }} -c conda-forge
|
||||
conda mambabuild . --python ${{ matrix.pyver.distver }} -c conda-forge --variants "{variant: ${{ matrix.variant }}}" --token ${{ secrets.ANACONDA_TOKEN }} --user ifcopenshell
|
||||
working-directory: ./conda
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
name: ci_py_only
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'test/**'
|
||||
- 'conda/**'
|
||||
- 'cmake/**'
|
||||
- '.github/workflows/ci-py-only.yml'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell' &&
|
||||
!contains(github.event.head_commit.message, 'skip ci')
|
||||
steps:
|
||||
- run: echo ok go
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install C++ dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt-get install --no-install-recommends \
|
||||
git cmake gcc g++ \
|
||||
libboost-date-time-dev \
|
||||
libboost-filesystem-dev \
|
||||
libboost-iostreams-dev \
|
||||
libboost-program-options-dev \
|
||||
libboost-regex-dev \
|
||||
libboost-system-dev \
|
||||
libboost-thread-dev \
|
||||
python3-all-dev python3-pip \
|
||||
swig libpcre3-dev libxml2-dev \
|
||||
libtbb-dev nlohmann-json3-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libhdf5-dev libcgal-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
with:
|
||||
key: ${GITHUB_WORKFLOW}
|
||||
|
||||
- name: Build ifcopenshell
|
||||
run: |
|
||||
sudo /usr/sbin/update-ccache-symlinks
|
||||
mkdir build && cd build
|
||||
PATH="/usr/lib/ccache:$PATH" cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
|
||||
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3_add1" \
|
||||
-DBUILD_CONVERT=Off \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DJSON_INCLUDE_DIR=/usr/include \
|
||||
-DCGAL_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_INCLUDE_DIR=/usr/include \
|
||||
-DMPFR_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
|
||||
../cmake
|
||||
sudo make -j $(nproc)
|
||||
sudo make install
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
sudo /usr/bin/python -m pip install -U pip
|
||||
sudo /usr/bin/python -m pip install xmlschema xsdata numpy lxml
|
||||
sudo /usr/bin/python -m pip install src/bcf --no-deps
|
||||
sudo /usr/bin/python -m pip install pytest
|
||||
sudo /usr/bin/python -m pip install isodate
|
||||
sudo /usr/bin/python -m pip install lark
|
||||
sudo /usr/bin/python -m pip install networkx
|
||||
sudo /usr/bin/python -m pip install tabulate
|
||||
sudo /usr/bin/python -m pip install python-dateutil
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
cd test
|
||||
sudo /usr/bin/python tests.py
|
||||
cd ../src/ifcopenshell-python
|
||||
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
||||
make test-safe
|
||||
+23
-20
@@ -31,12 +31,25 @@ jobs:
|
||||
- run: echo ok go
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil
|
||||
pip install src/bcf --no-deps
|
||||
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
|
||||
|
||||
- name: Install C++ dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
@@ -49,7 +62,6 @@ jobs:
|
||||
libboost-regex-dev \
|
||||
libboost-system-dev \
|
||||
libboost-thread-dev \
|
||||
python3-all-dev python3-pip \
|
||||
swig libpcre3-dev libxml2-dev \
|
||||
libtbb-dev nlohmann-json3-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
@@ -62,6 +74,9 @@ jobs:
|
||||
|
||||
- name: Build ifcopenshell
|
||||
run: |
|
||||
echo $Python3_ROOT_DIR
|
||||
echo ${{ env.pythonLocation }}
|
||||
|
||||
mkdir build && cd build
|
||||
cmake \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
@@ -71,9 +86,9 @@ jobs:
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
|
||||
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3_add1" \
|
||||
-DGLTF_SUPPORT=On \
|
||||
@@ -102,23 +117,11 @@ jobs:
|
||||
grep 0$ statuses
|
||||
grep 0$ statuses | wc -l
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
sudo /usr/bin/python -m pip install -U pip
|
||||
sudo /usr/bin/python -m pip install xmlschema xsdata numpy lxml
|
||||
sudo /usr/bin/python -m pip install src/bcf --no-deps
|
||||
sudo /usr/bin/python -m pip install pytest
|
||||
sudo /usr/bin/python -m pip install isodate
|
||||
sudo /usr/bin/python -m pip install lark
|
||||
sudo /usr/bin/python -m pip install networkx
|
||||
sudo /usr/bin/python -m pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
|
||||
sudo /usr/bin/python -m pip install tabulate
|
||||
sudo /usr/bin/python -m pip install python-dateutil
|
||||
|
||||
- name: Test
|
||||
- name: Test ifcopenshell-python
|
||||
run: |
|
||||
cd test
|
||||
sudo /usr/bin/python tests.py
|
||||
python tests.py
|
||||
cd ../src/ifcopenshell-python
|
||||
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
||||
make test-safe
|
||||
|
||||
@@ -96,6 +96,8 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
|
||||
# apple
|
||||
.DS_Store
|
||||
|
||||
# clangd config
|
||||
.clangd
|
||||
# clangd cache
|
||||
.cache
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
|
||||
URL_BLENDER_CMAKE = "https://raw.githubusercontent.com/blender/blender/{}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
|
||||
RE_BLENDER_VERSION_MIN_MAJ = r"Latest Version.+<span>Blender (\d+\.\d+)\..+</span>"
|
||||
RE_BLENDER_VERSION_MIN_MAJ_PAT = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
|
||||
RE_BLENDER_PYTHON_VERSION_MAJ_MIN = r"SET\(_PYTHON_VERSION_SUPPORTED (\d+\.\d+)\)"
|
||||
RE_BLENDER_PYTHON_VERSION_MAJ_MIN = r"(?:set|SET)\(_PYTHON_VERSION_SUPPORTED (\d+\.\d+)\)"
|
||||
|
||||
|
||||
if sys.argv[1] == "--do_choco_release?":
|
||||
|
||||
+23
-15
@@ -585,23 +585,31 @@ if(HDF5_SUPPORT)
|
||||
|
||||
if(WIN32)
|
||||
# Windows
|
||||
set(zlib_post zlib)
|
||||
set(lib_ext lib)
|
||||
elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
set(HDF5_LIBRARIES)
|
||||
find_package(HDF5 COMPONENTS CXX)
|
||||
if (HDF5_FOUND)
|
||||
set(HDF5_LIBRARIES ${HDF5_CXX_LIBRARIES})
|
||||
endif()
|
||||
list(APPEND HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/zlib.lib"
|
||||
)
|
||||
else()
|
||||
# linux
|
||||
set(zlib_post libz)
|
||||
set(lib_ext so)
|
||||
endif()
|
||||
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
else()
|
||||
# linux
|
||||
set(zlib_post libz)
|
||||
set(lib_ext so)
|
||||
endif()
|
||||
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
compile:
|
||||
conda activate conda-build && conda-build -c conda-forge . --keep-old-work --python 3.10.5
|
||||
|
||||
debug:
|
||||
conda activate conda-build && conda-debug -c conda-forge . --python 3.10.4
|
||||
|
||||
env:
|
||||
conda env update --f environment.yml --prune
|
||||
@@ -1,2 +1,6 @@
|
||||
CONDA_BUILD_SYSROOT:
|
||||
- /opt/MacOSX10.13.sdk # [osx]
|
||||
- /opt/MacOSX10.13.sdk # [osx]
|
||||
|
||||
variant:
|
||||
- novtk
|
||||
- all
|
||||
@@ -1,13 +0,0 @@
|
||||
name: conda-build
|
||||
channels:
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- conda-build
|
||||
- conda-verify
|
||||
- anaconda-client
|
||||
- ninja
|
||||
- doxygen
|
||||
- ripgrep
|
||||
- pip
|
||||
- pip:
|
||||
- lief==0.13
|
||||
+12
-7
@@ -1,6 +1,10 @@
|
||||
{% set name = "ifcopenshell" %}
|
||||
{% set version = "0.7.0" %}
|
||||
{% set build = 1 %}
|
||||
|
||||
# Higher number -> Always prioritize "novtk" variant over "all" variant
|
||||
{% set build = build + 200 %} # [variant == "novtk"]
|
||||
{% set build = build + 100 %} # [variant == "all"]
|
||||
|
||||
package:
|
||||
name: {{ name }}
|
||||
@@ -10,8 +14,9 @@ source:
|
||||
path: ..
|
||||
|
||||
build:
|
||||
string: py{{ CONDA_PY }}_{{ variant }}_h{{ PKG_HASH }}_{{ build }}
|
||||
binary_relocation: false [osx]
|
||||
number: 1
|
||||
number: {{ build }}
|
||||
|
||||
requirements:
|
||||
build:
|
||||
@@ -19,12 +24,12 @@ requirements:
|
||||
- {{ compiler('cxx') }}
|
||||
- ninja >=1.10.2
|
||||
- cmake
|
||||
- swig >=4.0.2
|
||||
- swig 4.1.1
|
||||
|
||||
host:
|
||||
- python
|
||||
- boost-cpp
|
||||
- occt ==7.7.0
|
||||
- occt 7.7.2 *{{ variant }}*
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
@@ -36,11 +41,11 @@ requirements:
|
||||
|
||||
run:
|
||||
- python
|
||||
- {{ pin_compatible('occt', max_pin='x.x.x') }}
|
||||
- occt 7.7.2 *{{ variant }}*
|
||||
- {{ pin_compatible('cgal-cpp', max_pin='x.x.x') }}
|
||||
- {{ pin_compatible('boost-cpp', max_pin='x.x.x') }}
|
||||
- {{ pin_compatible('hdf5', max_pin='x.x.x') }}
|
||||
- libxml2
|
||||
- hdf5
|
||||
- mpfr
|
||||
- gmp # [unix]
|
||||
- mpir # [win]
|
||||
@@ -52,12 +57,12 @@ test:
|
||||
- ifcopenshell
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
home: https://ifcopenshell.org
|
||||
license: LGPL-3.0-or-later
|
||||
license_file: COPYING
|
||||
summary: 'IfcOpenShell is a library to support the IFC file format'
|
||||
description: |
|
||||
IfcOpenShell is an open source (LGPL) software library for
|
||||
working with the Industry Foundation Classes (IFC) file format.
|
||||
doc_url: http://ifcopenshell.org/
|
||||
doc_url: https://ifcopenshell.org/
|
||||
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
|
||||
+4
-3
@@ -76,7 +76,7 @@ PROJECT_NAME = "IfcOpenShell"
|
||||
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
|
||||
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
|
||||
|
||||
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.0"]
|
||||
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.7", "3.12.1"]
|
||||
JSON_VERSION = "v3.6.1"
|
||||
OCE_VERSION = "0.18.3"
|
||||
OCCT_VERSION = "7.5.3"
|
||||
@@ -892,8 +892,9 @@ if "IfcOpenShell-Python" in targets:
|
||||
module_dir = os.path.dirname(run([python_executable, "-c", "import inspect, ifcopenshell; print(inspect.getfile(ifcopenshell))"]))
|
||||
|
||||
if platform.system() != "Darwin":
|
||||
# TODO: This symbol name depends on the Python version?
|
||||
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", "_ifcopenshell_wrapper.so"], cwd=module_dir)
|
||||
if BUILD_CFG == "Release":
|
||||
# TODO: This symbol name depends on the Python version?
|
||||
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", "_ifcopenshell_wrapper.so"], cwd=module_dir)
|
||||
|
||||
return module_dir
|
||||
|
||||
|
||||
+82
-40
@@ -50,24 +50,31 @@ PYLIBDIR:=python3.10
|
||||
PYNUMBER:=310
|
||||
PYPI_VERSION:=3.10
|
||||
endif
|
||||
ifeq ($(PYVERSION), py311)
|
||||
PYLIBDIR:=python3.11
|
||||
PYNUMBER:=311
|
||||
PYPI_VERSION:=3.11
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), linux)
|
||||
PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
|
||||
LXML_ARCH:=manylinux_2_28_x86_64
|
||||
SHAPELY_ARCH:=manylinux_2_17_x86_64.manylinux2014_x86_64
|
||||
PILLOW_ARCH:=manylinux_2_28_x86_64
|
||||
ifeq ($(PYVERSION), py39)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/linux-64/hpp-fcl-2.3.4-py39h40a70d0_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/linux-64/eigenpy-3.1.0-py39hdfdd6bb_0.conda
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py39h359ec1e_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py39hb1babde_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py39h7c9e3ff_4.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/2d/f2/8ec281d357e8bb7d08dc8d727f0e4c8ef3dae7d3fa75c69c8e452bb82d50/shapely-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/01/61/3ff85fb4bb596ce3d223c8fcf93c8df5c12bc8899dfb4fb3cb1c5b20dd5f/Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/linux-64/hpp-fcl-2.3.4-py310h995690b_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/linux-64/eigenpy-3.1.0-py310hf02b7e0_0.conda
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py310hd002f03_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py310hcfe0b05_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py310hc4a4660_4.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/25/1e/19b46d8e8881fe0df2e20945d51919eeb1817836d62a90efa8506530e45c/lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/a8/a5/403728b5614b28083f6424dfdefec5fcf58068495fb03bb08532671c642f/shapely-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/f6/51/320986ebd6d46a0e95c2240468ced73153b691ce07617078bcdf30c609ec/Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py311)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py311h04f086a_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py311h995a085_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py311h59ea3da_3.tar.bz2
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2
|
||||
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/linux-64/qhull-2020.2-h4bd325d_2.tar.bz2
|
||||
@@ -78,21 +85,25 @@ endif
|
||||
|
||||
ifeq ($(PLATFORM), macos)
|
||||
PYPI_PLATFORM:=--platform macosx_10_9_x86_64
|
||||
SHAPELY_ARCH:=macosx_10_9_x86_64
|
||||
PILLOW_ARCH:=macosx_10_10_x86_64
|
||||
ifeq ($(PYVERSION), py39)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/osx-64/hpp-fcl-2.3.4-py39hd85b194_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/osx-64/eigenpy-3.1.0-py39hc4d6e28_0.conda
|
||||
LXML_ARCH:=macosx_11_0_x86_64
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py39h26350b9_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py39h3323527_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py39h953a6b8_4.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/36/a4/7e542a209f862f967d7cb8e939eff155f4294a27d17e16441fb8bdd51a2c/shapely-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/88/7a/ddfe28b485b623361457d4783007c1f9ba83a87f93e7fec32f64793efb6c/Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/osx-64/hpp-fcl-2.3.4-py310h1db6f5f_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/osx-64/eigenpy-3.1.0-py310h43da829_0.conda
|
||||
LXML_ARCH:=macosx_11_0_x86_64
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py310ha216469_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py310h33a8c9b_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py310h3e792ce_4.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/a1/44/17b7dac7a18807d30e2fe10c3328c152808f5464565e230bfd0e77f178c6/lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/1f/2a/dc3353c2431cf53e8d04bb8fba27e584410ca3435c9c85f76d71bf0c0e80/shapely-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/d8/60/b13c00d403f34110e96c1b5c0afa73ce461efe3fe960c3a7e3e7fe190d82/Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py311)
|
||||
LXML_ARCH:=macosx_11_0_universal2
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py311hebdd1a2_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py311h62e90b6_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py311h349b758_4.tar.bz2
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2
|
||||
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-64/qhull-2020.2-h940c156_2.tar.bz2
|
||||
@@ -103,21 +114,23 @@ endif
|
||||
|
||||
ifeq ($(PLATFORM), macosm1)
|
||||
PYPI_PLATFORM:=--platform macosx_11_0_arm64
|
||||
LXML_ARCH:=macosx_11_0_universal2
|
||||
SHAPELY_ARCH:=macosx_11_0_arm64
|
||||
PILLOW_ARCH:=macosx_11_0_arm64
|
||||
ifeq ($(PYVERSION), py39)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/osx-arm64/hpp-fcl-2.3.4-py39hc34188a_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/osx-arm64/eigenpy-3.1.0-py39h13cfc01_0.conda
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py39h82b2ff2_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py39h3a9e884_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h99de9ae_4.tar.bz2
|
||||
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h9eb174b_0.tar.bz2
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/ea/aa/45fbd031edf3149cb767d8b9f9db45d5faf0324d743c6b8fb0298cc022d0/shapely-2.0.1-cp39-cp39-macosx_11_0_arm64.whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/osx-arm64/hpp-fcl-2.3.4-py310h46fc4cd_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/osx-arm64/eigenpy-3.1.0-py310ha2643af_0.conda
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py310hdb6c3eb_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py310h96e9823_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h629746b_4.tar.bz2
|
||||
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/ec/41/d59208743e737184e1b403e95a937aebb022b8459e99efbcd5208fc8be46/shapely-2.0.1-cp310-cp310-macosx_11_0_arm64.whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/0c/5f/117b653cad585f3aedfe0de996c292e67d4b020ed77f652e5a6c8c24f908/Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py311)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py311hc0c29b9_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py311h24fa84d_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py311h5c64717_4.tar.bz2
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-arm64/assimp-5.0.1-h0f81e16_7.tar.bz2
|
||||
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-arm64/qhull-2020.2-hc021e02_2.tar.bz2
|
||||
@@ -128,21 +141,23 @@ endif
|
||||
|
||||
ifeq ($(PLATFORM), win)
|
||||
PYPI_PLATFORM:=--platform win_amd64
|
||||
LXML_ARCH:=win_amd64
|
||||
SHAPELY_ARCH:=win_amd64
|
||||
PILLOW_ARCH:=win_amd64
|
||||
ifeq ($(PYVERSION), py39)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/win-64/hpp-fcl-2.3.4-py39h39b25cb_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/win-64/eigenpy-3.1.0-py39hb6c915b_0.conda
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py39hf3ebc58_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py39ha1f6a47_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py39hea4d8d1_4.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/72/d4/426ecb8849c47c3e370c87aa0ac05d85768df917ffea27fcd6686a5e6495/lxml-4.6.3-cp39-cp39-win_amd64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/a7/ae/eab645c4075093584b7a65ab71cb8ff4563a015bd935c9b55dba14b2c1eb/shapely-2.0.1-cp39-cp39-win_amd64.whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/19/3f/b4d4bcf05dbcbe07f2e9613a8f4180c297395e73a91d8ad22c32c6624f8c/Pillow-9.2.0-cp39-cp39-win_amd64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/win-64/hpp-fcl-2.3.4-py310heb59267_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/win-64/eigenpy-3.1.0-py310hd146abe_0.conda
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py310h1bf3f0a_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py310h7fb871d_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py310h220cb41_4.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/f6/71/65c80a4caa1617a4c6e8fe1500cebb179db96232e2f623bfe6a1f4294e39/lxml-4.8.0-cp310-cp310-win_amd64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/81/8a/7ac076a86b2632f1872284c5e60ed5f2fc26094875a85b35d9fa17b52504/shapely-2.0.1-cp310-cp310-win_amd64.whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/02/55/67a3c17b9e7d972ed8c246f104da99ca4f3ea42fba566697e479011b84b6/Pillow-9.2.0-cp310-cp310-win_amd64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py311)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py311hd263b2c_0.conda
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py311hd354db1_0.conda
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py311h12feb9d_3.tar.bz2
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.2.5/download/win-64/assimp-5.2.5-h4dcb625_0.tar.bz2
|
||||
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/win-64/qhull-2020.2-h70d2c02_2.tar.bz2
|
||||
@@ -151,6 +166,33 @@ BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/win-64/
|
||||
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/win-64/zlib-1.2.11-h62dcd97_1010.tar.bz2
|
||||
endif
|
||||
|
||||
LXML_NAME:=lxml
|
||||
LXML_VER:=4.9.4
|
||||
SHAPELY_NAME:=shapely
|
||||
SHAPELY_VER:=2.0.2
|
||||
PILLOW_NAME:=pillow
|
||||
PILLOW_PKG_NAME:=Pillow
|
||||
PILLOW_VER:=9.5.0
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/s/$(SHAPELY_NAME)/$(SHAPELY_NAME)-$(SHAPELY_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(SHAPELY_ARCH).whl
|
||||
PILLOW_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/p/$(PILLOW_NAME)/$(PILLOW_PKG_NAME)-$(PILLOW_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(PILLOW_ARCH).whl
|
||||
|
||||
ifeq ($(PLATFORM), macosm1)
|
||||
ifeq ($(PYVERSION), py39)
|
||||
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h0520ce3_1.tar.bz2
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
|
||||
endif
|
||||
ifeq ($(PYVERSION), py311)
|
||||
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py311h246f609_1.tar.bz2
|
||||
endif
|
||||
else
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/l/$(LXML_NAME)/$(LXML_NAME)-$(LXML_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(LXML_ARCH).whl
|
||||
endif
|
||||
$(info $$LXML_URL is [${LXML_URL}])
|
||||
$(info $$SHAPELY_URL is [${SHAPELY_URL}])
|
||||
$(info $$PILLOW_URL is [${PILLOW_URL}])
|
||||
|
||||
.PHONY: bump
|
||||
bump:
|
||||
cd . && $(SED) "s/$(OLD)/$(NEW)/" Makefile
|
||||
|
||||
@@ -44,6 +44,7 @@ modules = {
|
||||
"spatial": None,
|
||||
"void": None,
|
||||
"aggregate": None,
|
||||
"nest": None,
|
||||
"geometry": None,
|
||||
"fm": None,
|
||||
"resource": None,
|
||||
@@ -122,6 +123,8 @@ classes = [
|
||||
prop.BIMCollectionProperties,
|
||||
prop.BIMMaterialProperties,
|
||||
prop.BIMMeshProperties,
|
||||
prop.BIMFacet,
|
||||
prop.BIMFilterGroup,
|
||||
ui.BIM_UL_generic,
|
||||
ui.BIM_UL_topics,
|
||||
ui.BIM_ADDON_preferences,
|
||||
@@ -151,7 +154,6 @@ classes = [
|
||||
ui.BIM_PT_tab_references,
|
||||
# Services and systems
|
||||
ui.BIM_PT_tab_services,
|
||||
ui.BIM_PT_tab_services_object,
|
||||
ui.BIM_PT_tab_zones,
|
||||
# Structural analysis
|
||||
ui.BIM_PT_tab_structural,
|
||||
@@ -179,6 +181,7 @@ addon_keymaps = []
|
||||
icons = None
|
||||
is_registering = False
|
||||
last_commit_hash = "8888888"
|
||||
overridden_scene_panels = dict()
|
||||
|
||||
|
||||
def on_register(scene):
|
||||
@@ -220,8 +223,8 @@ def register():
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY')
|
||||
kmi = km.keymap_items.new('bim.switch_tab', 'TAB', 'PRESS', ctrl=True)
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.switch_tab", "TAB", "PRESS", ctrl=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
global icons
|
||||
@@ -239,12 +242,14 @@ def register():
|
||||
global last_commit_hash
|
||||
try:
|
||||
import git
|
||||
|
||||
path = Path(__file__).resolve().parent
|
||||
repo = git.Repo(str(path), search_parent_directories=True)
|
||||
last_commit_hash = repo.head.object.hexsha
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def unregister():
|
||||
global icons
|
||||
|
||||
@@ -281,16 +286,8 @@ def unregister():
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
for panel in [
|
||||
"SCENE_PT_scene",
|
||||
"SCENE_PT_unit",
|
||||
"SCENE_PT_physics",
|
||||
"SCENE_PT_rigid_body_world",
|
||||
"SCENE_PT_audio",
|
||||
"SCENE_PT_keying_sets",
|
||||
"SCENE_PT_custom_props",
|
||||
]:
|
||||
try:
|
||||
bpy.utils.unregister_class(getattr(handler, f"Override_{panel}"))
|
||||
except:
|
||||
pass
|
||||
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]
|
||||
|
||||
@@ -21,6 +21,7 @@ import bpy
|
||||
import json
|
||||
import addon_utils
|
||||
import ifcopenshell.api.owner.settings
|
||||
import blenderbim.bim
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.owner as core_owner
|
||||
from bpy.app.handlers import persistent
|
||||
@@ -59,6 +60,10 @@ def name_callback(obj, data):
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
return
|
||||
|
||||
if obj.BIMObjectProperties.is_renaming:
|
||||
obj.BIMObjectProperties.is_renaming = False
|
||||
return
|
||||
|
||||
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if "/" in obj.name:
|
||||
object_name = obj.name
|
||||
@@ -249,74 +254,58 @@ def viewport_shading_changed_callback(area):
|
||||
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
|
||||
|
||||
|
||||
if getattr(bpy.types, "SCENE_PT_scene"):
|
||||
@classmethod
|
||||
def poll_check_blender_tab(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
class Override_SCENE_PT_scene(bpy.types.SCENE_PT_scene):
|
||||
bl_idname = "SCENE_PT_scene_override"
|
||||
|
||||
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 poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
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
|
||||
|
||||
|
||||
if getattr(bpy.types, "SCENE_PT_unit"):
|
||||
|
||||
class Override_SCENE_PT_unit(bpy.types.SCENE_PT_unit):
|
||||
bl_idname = "SCENE_PT_unit_override"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
|
||||
if getattr(bpy.types, "SCENE_PT_physics"):
|
||||
|
||||
class Override_SCENE_PT_physics(bpy.types.SCENE_PT_physics):
|
||||
bl_idname = "SCENE_PT_physics_override"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
|
||||
if getattr(bpy.types, "SCENE_PT_rigid_body_world"):
|
||||
|
||||
class Override_SCENE_PT_rigid_body_world(bpy.types.SCENE_PT_rigid_body_world):
|
||||
bl_idname = "SCENE_PT_rigid_body_world_override"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
|
||||
if getattr(bpy.types, "SCENE_PT_audio"):
|
||||
|
||||
class Override_SCENE_PT_audio(bpy.types.SCENE_PT_audio):
|
||||
bl_idname = "SCENE_PT_audio_override"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
|
||||
if getattr(bpy.types, "SCENE_PT_keying_sets"):
|
||||
|
||||
class Override_SCENE_PT_keying_sets(bpy.types.SCENE_PT_keying_sets):
|
||||
bl_idname = "SCENE_PT_keying_sets_override"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
|
||||
if getattr(bpy.types, "SCENE_PT_custom_props"):
|
||||
|
||||
class Override_SCENE_PT_custom_props(bpy.types.SCENE_PT_custom_props):
|
||||
bl_idname = "SCENE_PT_custom_props_override"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
# 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
|
||||
@@ -345,6 +334,9 @@ def load_post(scene):
|
||||
ifcopenshell.api.owner.settings.get_application = get_application
|
||||
AuthoringData.type_thumbnails = {}
|
||||
|
||||
if not bpy.context.preferences.addons["blenderbim"].preferences.should_setup_toolbar:
|
||||
tool.Blender.unregister_toolbar()
|
||||
|
||||
if bpy.context.preferences.addons["blenderbim"].preferences.should_setup_workspace:
|
||||
if "BIM" in bpy.data.workspaces:
|
||||
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
|
||||
@@ -354,22 +346,14 @@ def load_post(scene):
|
||||
# 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 [
|
||||
"SCENE_PT_scene",
|
||||
"SCENE_PT_unit",
|
||||
"SCENE_PT_physics",
|
||||
"SCENE_PT_rigid_body_world",
|
||||
"SCENE_PT_audio",
|
||||
"SCENE_PT_keying_sets",
|
||||
"SCENE_PT_custom_props",
|
||||
]:
|
||||
if getattr(bpy.types, panel, None):
|
||||
try:
|
||||
bpy.utils.register_class(globals()[f"Override_{panel}"])
|
||||
bpy.utils.unregister_class(getattr(bpy.types, panel))
|
||||
except:
|
||||
pass
|
||||
|
||||
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)
|
||||
# https://blender.stackexchange.com/questions/140644/how-can-make-the-state-of-a-boolean-property-relative-to-the-3d-view-area
|
||||
for screen in bpy.data.screens:
|
||||
if len(screen.BIMAreaProperties) == 20:
|
||||
|
||||
@@ -87,8 +87,15 @@ def import_attributes(ifc_class, props, data, callback=None):
|
||||
|
||||
# A more elegant attribute importer signature, intended to supersede import_attributes
|
||||
def import_attributes2(element, props, callback=None):
|
||||
for attribute in element.wrapped_data.declaration().as_entity().all_attributes():
|
||||
import_attribute(attribute, props, element.get_info(), callback=callback)
|
||||
if isinstance(element, str):
|
||||
attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes()
|
||||
info = {a.name(): None for a in attributes}
|
||||
info["type"] = element
|
||||
else:
|
||||
attributes = element.wrapped_data.declaration().as_entity().all_attributes()
|
||||
info = element.get_info()
|
||||
for attribute in attributes:
|
||||
import_attribute(attribute, props, info, callback=callback)
|
||||
|
||||
|
||||
def import_attribute(attribute, props, data, callback=None):
|
||||
@@ -260,12 +267,7 @@ def convert_property_group_from_si(property_group, skip_props=()):
|
||||
for prop_name in property_group.bl_rna.properties.keys():
|
||||
if prop_name in skip_props:
|
||||
continue
|
||||
prop_bl_rna = property_group.bl_rna.properties[prop_name]
|
||||
if prop_bl_rna.array_length > 0:
|
||||
prop_value = prop_bl_rna.default_array
|
||||
else:
|
||||
prop_value = prop_bl_rna.default
|
||||
|
||||
prop_value = tool.Blender.get_blender_prop_default_value(property_group, prop_name)
|
||||
if type(prop_value) is float:
|
||||
prop_value = prop_value * conversion_k
|
||||
elif type(prop_value) is bpy.types.bpy_prop_array:
|
||||
@@ -273,7 +275,7 @@ def convert_property_group_from_si(property_group, skip_props=()):
|
||||
setattr(property_group, prop_name, prop_value)
|
||||
|
||||
|
||||
def draw_filter(layout, props, data, module):
|
||||
def draw_filter(layout, filter_groups, data, module):
|
||||
if not data.is_loaded:
|
||||
data.load()
|
||||
|
||||
@@ -291,7 +293,7 @@ def draw_filter(layout, props, data, module):
|
||||
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
|
||||
row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module
|
||||
|
||||
for i, filter_group in enumerate(props.filter_groups):
|
||||
for i, filter_group in enumerate(filter_groups):
|
||||
box = layout.box()
|
||||
|
||||
row = box.row(align=True)
|
||||
|
||||
@@ -163,8 +163,12 @@ class IfcStore:
|
||||
if not IfcStore.get_file():
|
||||
return
|
||||
for obj in bpy.data.objects:
|
||||
if obj.library:
|
||||
continue
|
||||
IfcStore.relink_object(obj)
|
||||
for obj in bpy.data.materials:
|
||||
if obj.library:
|
||||
continue
|
||||
IfcStore.relink_object(obj)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -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 re
|
||||
import bpy
|
||||
@@ -38,7 +39,7 @@ from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
|
||||
|
||||
|
||||
class MaterialCreator:
|
||||
def __init__(self, ifc_import_settings, ifc_importer):
|
||||
def __init__(self, ifc_import_settings: IfcImportSettings, ifc_importer: IfcImporter):
|
||||
self.mesh = None
|
||||
self.obj = None
|
||||
self.materials = {}
|
||||
@@ -125,58 +126,28 @@ class MaterialCreator:
|
||||
return
|
||||
for style_id in style_ids:
|
||||
material = self.styles[style_id]
|
||||
ifc_coordinate_id = material.BIMMaterialProperties.ifc_coordinate_id
|
||||
if ifc_coordinate_id != 0:
|
||||
self.load_texture_map(tool.Ifc.get().by_id(ifc_coordinate_id))
|
||||
|
||||
def get_ifc_coordinate(material):
|
||||
texture_style = tool.Style.get_texture_style(material)
|
||||
if not texture_style:
|
||||
return
|
||||
for texture in texture_style.Textures or []:
|
||||
if coords := getattr(texture, "IsMappedBy", None):
|
||||
coords = coords[0]
|
||||
# IfcTextureCoordinateGenerator handled in the style shader graph
|
||||
if coords.is_a("IfcIndexedTextureMap"):
|
||||
return coords
|
||||
# TODO: support IfcTextureMap
|
||||
if coords.is_a("IfcTextureMap"):
|
||||
print(f"WARNING. IfcTextureMap texture coordinates is not supported.")
|
||||
return
|
||||
|
||||
if coords := get_ifc_coordinate(material):
|
||||
tool.Loader.load_indexed_texture_map(coords, self.mesh)
|
||||
if self.mesh.materials.find(material.name) == -1:
|
||||
self.mesh.materials.append(material)
|
||||
return True
|
||||
|
||||
def load_texture_map(self, coordinates):
|
||||
# Get a BMesh representation
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(self.mesh)
|
||||
uv_layer = bm.loops.layers.uv.verify()
|
||||
|
||||
# remap the faceset CoordList index to the vertices in blender mesh
|
||||
coordinates_remap = []
|
||||
for co in coordinates.MappedTo.Coordinates.CoordList:
|
||||
co = mathutils.Vector(co)
|
||||
index = next(v.index for v in bm.verts if (v.co - co).length_squared < 1e-5)
|
||||
coordinates_remap.append(index)
|
||||
|
||||
faces_remap = None
|
||||
texture_map = None
|
||||
if coordinates.is_a("IfcIndexedPolygonalTextureMap"):
|
||||
faces_remap = [
|
||||
[coordinates_remap[i - 1] for i in tex_coord_index.TexCoordsOf.CoordIndex]
|
||||
for tex_coord_index in coordinates.TexCoordIndices
|
||||
]
|
||||
texture_map = [tex_coord_index.TexCoordIndex for tex_coord_index in coordinates.TexCoordIndices]
|
||||
elif coordinates.is_a("IfcIndexedTriangleTextureMap"):
|
||||
faces_remap = [
|
||||
[coordinates_remap[i - 1] for i in triangle_face] for triangle_face in coordinates.MappedTo.CoordIndex
|
||||
]
|
||||
texture_map = coordinates.TexCoordIndex
|
||||
|
||||
# apply uv to each face
|
||||
for bface in bm.faces:
|
||||
face = [loop.vert.index for loop in bface.loops]
|
||||
# find the corresponding TexCoordIndex by matching ifc faceset with blender face
|
||||
# remap TexCoordIndex as the loop start may different from blender face
|
||||
texCoordIndex = next(
|
||||
[tex_coord_index[face_remap.index(i)] for i in face]
|
||||
for tex_coord_index, face_remap in zip(texture_map, faces_remap)
|
||||
if all(i in face_remap for i in face)
|
||||
)
|
||||
# apply uv to each loop
|
||||
for loop, i in zip(bface.loops, texCoordIndex):
|
||||
loop[uv_layer].uv = coordinates.TexCoords.TexCoordsList[i - 1]
|
||||
|
||||
# Finish up, write the bmesh back to the mesh
|
||||
bm.to_mesh(self.mesh)
|
||||
bm.free()
|
||||
|
||||
def assign_material_slots_to_faces(self):
|
||||
if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]:
|
||||
return
|
||||
@@ -254,6 +225,8 @@ class IfcImporter:
|
||||
self.profile_code("Loading file")
|
||||
self.calculate_unit_scale()
|
||||
self.profile_code("Calculate unit scale")
|
||||
self.process_context_filter()
|
||||
self.profile_code("Process context filter")
|
||||
self.calculate_model_offset()
|
||||
self.profile_code("Calculate model offset")
|
||||
self.predict_dense_mesh()
|
||||
@@ -264,8 +237,6 @@ class IfcImporter:
|
||||
self.profile_code("Create project")
|
||||
self.process_element_filter()
|
||||
self.profile_code("Process element filter")
|
||||
self.process_context_filter()
|
||||
self.profile_code("Process context filter")
|
||||
self.create_collections()
|
||||
self.profile_code("Create collections")
|
||||
self.create_materials()
|
||||
@@ -296,7 +267,7 @@ class IfcImporter:
|
||||
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
|
||||
self.clean_mesh()
|
||||
self.profile_code("Mesh cleaning")
|
||||
if self.ifc_import_settings.should_merge_materials_by_colour or len(self.material_creator.materials) > 300:
|
||||
if self.ifc_import_settings.should_merge_materials_by_colour:
|
||||
self.merge_materials_by_colour()
|
||||
self.profile_code("Merging by colour")
|
||||
self.set_default_context()
|
||||
@@ -494,7 +465,7 @@ class IfcImporter:
|
||||
|
||||
rep = representation
|
||||
while True:
|
||||
if rep.Items and rep.Items[0].is_a("IfcMappedItem"):
|
||||
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
|
||||
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(rep.Items[0])
|
||||
if not np.allclose(rep_matrix, np.eye(4)):
|
||||
matrix = rep_matrix @ matrix
|
||||
@@ -547,7 +518,7 @@ class IfcImporter:
|
||||
return True
|
||||
|
||||
def is_native_swept_disk_solid(self, element, representation):
|
||||
items = representation.Items or [] # Be forgiving of invalid IFCs because Revit :(
|
||||
items = [i["item"] for i in ifcopenshell.util.representation.resolve_items(representation)]
|
||||
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
|
||||
if tool.Blender.Modifier.is_railing(element):
|
||||
return False
|
||||
@@ -563,6 +534,7 @@ class IfcImporter:
|
||||
return False
|
||||
|
||||
def is_native_faceted_brep(self, representation):
|
||||
# TODO handle mapped items
|
||||
for i in representation.Items:
|
||||
if i.is_a() != "IfcFacetedBrep":
|
||||
return False
|
||||
@@ -615,11 +587,11 @@ class IfcImporter:
|
||||
project = self.file.by_type("IfcContext")[0]
|
||||
site = self.find_decomposed_ifc_class(project, "IfcSite")
|
||||
if site and self.is_element_far_away(site):
|
||||
return self.guess_georeferencing(site)
|
||||
return self.guess_false_origin_and_project_north(site)
|
||||
building = self.find_decomposed_ifc_class(project, "IfcBuilding")
|
||||
if building and self.is_element_far_away(building):
|
||||
return self.guess_georeferencing(building)
|
||||
return self.guess_absolute_coordinate()
|
||||
return self.guess_false_origin_and_project_north(building)
|
||||
return self.guess_false_origin()
|
||||
|
||||
def set_manual_blender_offset(self):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
@@ -628,7 +600,7 @@ class IfcImporter:
|
||||
props.blender_orthogonal_height = str(self.ifc_import_settings.false_origin[2])
|
||||
props.has_blender_offset = True
|
||||
|
||||
def guess_georeferencing(self, element):
|
||||
def guess_false_origin_and_project_north(self, element):
|
||||
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
|
||||
return
|
||||
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
@@ -643,7 +615,7 @@ class IfcImporter:
|
||||
props.blender_x_axis_ordinate = str(placement[1][0])
|
||||
props.has_blender_offset = True
|
||||
|
||||
def guess_absolute_coordinate(self):
|
||||
def guess_false_origin(self):
|
||||
# Civil BIM applications like to work in absolute coordinates, where the
|
||||
# ObjectPlacement is usually 0,0,0 (but not always, so we'll need to
|
||||
# check for the actual transformation) but each individual coordinate of
|
||||
@@ -667,6 +639,10 @@ class IfcImporter:
|
||||
elements_checked += 1
|
||||
if elements_checked > element_checking_threshold:
|
||||
return
|
||||
if element.ObjectPlacement and element.ObjectPlacement.is_a("IfcLocalPlacement"):
|
||||
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)[:, 3][0:3]
|
||||
if self.is_point_far_away(placement, is_meters=False):
|
||||
return placement
|
||||
if not self.does_element_likely_have_geometry_far_away(element):
|
||||
continue
|
||||
shape = self.create_generic_shape(element)
|
||||
@@ -1316,12 +1292,17 @@ class IfcImporter:
|
||||
curve.resolution_u = 2
|
||||
polyline = curve.splines.new("POLY")
|
||||
|
||||
for item in native_data["representation"].Items:
|
||||
for item_data in ifcopenshell.util.representation.resolve_items(native_data["representation"]):
|
||||
item = item_data["item"]
|
||||
matrix = item_data["matrix"]
|
||||
matrix[0][3] *= self.unit_scale
|
||||
matrix[1][3] *= self.unit_scale
|
||||
matrix[2][3] *= self.unit_scale
|
||||
# TODO: support inner radius, start param, and end param
|
||||
geometry = self.create_generic_shape(item.Directrix)
|
||||
e = geometry.edges
|
||||
v = geometry.verts
|
||||
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
||||
vertices = [list(matrix @ [v[i], v[i + 1], v[i + 2], 1]) for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
for edge in edges:
|
||||
@@ -1459,11 +1440,10 @@ class IfcImporter:
|
||||
types_collection.hide_viewport = False
|
||||
bpy.context.view_layer.objects.active = last_obj
|
||||
|
||||
context_override = {}
|
||||
bpy.ops.object.editmode_toggle(context_override)
|
||||
bpy.ops.mesh.tris_convert_to_quads(context_override)
|
||||
bpy.ops.mesh.normals_make_consistent(context_override)
|
||||
bpy.ops.object.editmode_toggle(context_override)
|
||||
bpy.ops.object.editmode_toggle()
|
||||
bpy.ops.mesh.tris_convert_to_quads()
|
||||
bpy.ops.mesh.normals_make_consistent()
|
||||
bpy.ops.object.editmode_toggle()
|
||||
|
||||
types_collection.hide_viewport = True
|
||||
bpy.context.view_layer.objects.active = last_obj
|
||||
@@ -1527,12 +1507,12 @@ class IfcImporter:
|
||||
obj = self.create_product(self.project["ifc"])
|
||||
self.project["blender"].objects.link(obj)
|
||||
self.project["blender"].BIMCollectionProperties.obj = obj
|
||||
obj.BIMObjectProperties.collection = self.project["blender"]
|
||||
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
|
||||
|
||||
def create_collections(self):
|
||||
self.create_spatial_decomposition_collections()
|
||||
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
|
||||
self.create_aggregate_collections()
|
||||
self.create_aggregate_and_nest_collections()
|
||||
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
|
||||
pass
|
||||
|
||||
@@ -1568,16 +1548,29 @@ class IfcImporter:
|
||||
for rel_aggregate in element.IsDecomposedBy:
|
||||
self.create_spatial_decomposition_collection(collection, rel_aggregate.RelatedObjects)
|
||||
|
||||
def create_aggregate_collections(self):
|
||||
def create_aggregate_and_nest_collections(self):
|
||||
if self.ifc_import_settings.has_filter:
|
||||
rel_aggregates = [e.IsDecomposedBy[0] for e in self.elements if e.IsDecomposedBy]
|
||||
rel_aggregates += [e.Decomposes[0] for e in self.elements if e.Decomposes]
|
||||
rel_aggregates = set(rel_aggregates)
|
||||
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 element.IsNestedBy:
|
||||
if [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")]:
|
||||
rel_aggregates.add(element.IsNestedBy[0])
|
||||
elif element.Nests:
|
||||
rel_aggregates.add(element.Nests[0])
|
||||
else:
|
||||
rel_aggregates = [
|
||||
a
|
||||
for a in self.file.by_type("IfcRelAggregates")
|
||||
if a.RelatingObject.is_a("IfcElement") or a.RelatingObject.is_a("IfcElementType")
|
||||
r
|
||||
for r in self.file.by_type("IfcRelAggregates")
|
||||
if r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType")
|
||||
] + [
|
||||
r
|
||||
for r in self.file.by_type("IfcRelNests")
|
||||
if (r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType"))
|
||||
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
|
||||
]
|
||||
|
||||
if len(rel_aggregates) > 10000:
|
||||
@@ -1682,6 +1675,9 @@ class IfcImporter:
|
||||
elif getattr(element, "Decomposes", None):
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
return self.collections[aggregate.GlobalId].objects.link(obj)
|
||||
elif getattr(element, "Nests", None):
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
return self.collections[nest.GlobalId].objects.link(obj)
|
||||
else:
|
||||
return self.place_object_in_spatial_decomposition_collection(element, obj)
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.BIM_OT_assign_object,
|
||||
operator.BIM_OT_unassign_object,
|
||||
operator.BIM_OT_enable_editing_aggregate,
|
||||
operator.BIM_OT_disable_editing_aggregate,
|
||||
operator.BIM_OT_add_aggregate,
|
||||
operator.BIM_OT_select_parts,
|
||||
operator.BIM_OT_select_aggregate,
|
||||
operator.BIM_OT_add_part_to_object,
|
||||
operator.BIM_OT_aggregate_assign_object,
|
||||
operator.BIM_OT_disable_editing_aggregate,
|
||||
operator.BIM_OT_enable_editing_aggregate,
|
||||
operator.BIM_OT_select_aggregate,
|
||||
operator.BIM_OT_select_parts,
|
||||
operator.BIM_OT_aggregate_unassign_object,
|
||||
prop.BIMObjectAggregateProperties,
|
||||
ui.BIM_PT_aggregate,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ class AggregateData:
|
||||
"has_relating_object": cls.has_relating_object(),
|
||||
"relating_object_label": cls.get_relating_object_label(),
|
||||
"has_related_objects": cls.has_related_objects(),
|
||||
"related_objects_amount": cls.get_related_objects_amount(),
|
||||
"total_parts": cls.total_parts(),
|
||||
"ifc_class": cls.ifc_class(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
@@ -59,7 +59,7 @@ class AggregateData:
|
||||
return ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
|
||||
@classmethod
|
||||
def get_related_objects_amount(cls):
|
||||
def total_parts(cls):
|
||||
parts = cls.get_related_objects()
|
||||
return len(parts) if parts else 0
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ class Operator:
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_assign_object(bpy.types.Operator, Operator):
|
||||
class BIM_OT_aggregate_assign_object(bpy.types.Operator, Operator):
|
||||
"""Create aggregation relationship between two ifc elements"""
|
||||
|
||||
bl_idname = "bim.assign_object"
|
||||
bl_label = "Assign Object"
|
||||
bl_idname = "bim.aggregate_assign_object"
|
||||
bl_label = "Assign Object To Aggregation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
relating_object: bpy.props.IntProperty()
|
||||
|
||||
@@ -67,11 +67,11 @@ class BIM_OT_assign_object(bpy.types.Operator, Operator):
|
||||
self.report({"ERROR"}, f" Cannot aggregate {obj.name} to {relating_obj.name}")
|
||||
|
||||
|
||||
class BIM_OT_unassign_object(bpy.types.Operator, Operator):
|
||||
class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
|
||||
"""Remove aggregation relationship between two ifc elements"""
|
||||
|
||||
bl_idname = "bim.unassign_object"
|
||||
bl_label = "Unassign Object"
|
||||
bl_idname = "bim.aggregate_unassign_object"
|
||||
bl_label = "Unassign Object From Aggregation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -164,6 +164,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def create_aggregate(self, context, ifc_class):
|
||||
aggregate = bpy.data.objects.new("Assembly", None)
|
||||
aggregate.location = context.scene.cursor.location
|
||||
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
|
||||
return aggregate
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class BIM_PT_aggregate(Panel):
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "relating_object", text="")
|
||||
if props.relating_object:
|
||||
op = row.operator("bim.assign_object", icon="CHECKMARK", text="")
|
||||
op = row.operator("bim.aggregate_assign_object", icon="CHECKMARK", text="")
|
||||
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
|
||||
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
|
||||
else:
|
||||
@@ -65,15 +65,21 @@ class BIM_PT_aggregate(Panel):
|
||||
op.obj = context.active_object.name
|
||||
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.add_aggregate", icon="ADD", text="")
|
||||
op = row.operator("bim.unassign_object", icon="X", text="")
|
||||
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
|
||||
else:
|
||||
row.label(text="No Aggregate", icon="TRIA_UP")
|
||||
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.add_aggregate", icon="ADD", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
parts = AggregateData.data["related_objects_amount"]
|
||||
row.label(text=f"{parts or 'No'} Part{'s' if parts > 1 else ''}", icon="TRIA_DOWN")
|
||||
total_parts = AggregateData.data["total_parts"]
|
||||
if total_parts == 0:
|
||||
row.label(text="No Parts", icon="TRIA_DOWN")
|
||||
elif total_parts == 1:
|
||||
row.label(text="1 Part", icon="TRIA_DOWN")
|
||||
else:
|
||||
row.label(text=f"{total_parts} Parts", icon="TRIA_DOWN")
|
||||
|
||||
if AggregateData.data["has_related_objects"]:
|
||||
op = row.operator("bim.select_parts", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.obj = context.active_object.name
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import bcf
|
||||
import bcf.v2.bcfxml
|
||||
|
||||
@@ -23,12 +25,31 @@ import bcf.v2.bcfxml
|
||||
class BcfStore:
|
||||
bcfxml = None
|
||||
|
||||
@staticmethod
|
||||
def get_bcfxml():
|
||||
if not BcfStore.bcfxml:
|
||||
BcfStore.bcfxml = bcf.v2.bcfxml.BcfXml()
|
||||
return BcfStore.bcfxml
|
||||
@classmethod
|
||||
def get_bcfxml(cls):
|
||||
if not cls.bcfxml:
|
||||
bcf_filepath = bpy.context.scene.BCFProperties.bcf_file
|
||||
if not os.path.isabs(bcf_filepath):
|
||||
bcf_filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), bcf_filepath))
|
||||
if bcf_filepath:
|
||||
try:
|
||||
cls.bcfxml = bcf.bcfxml.load(bcf_filepath)
|
||||
except:
|
||||
# there will be a plenty of "Permission denied" errors
|
||||
# as many poll() methods will try to access the bcfxml simultaneously
|
||||
# the first time it's loaded
|
||||
pass
|
||||
return cls.bcfxml
|
||||
|
||||
@classmethod
|
||||
def set(cls, bcfxml):
|
||||
def set(cls, bcfxml, filepath):
|
||||
cls.bcfxml = bcfxml
|
||||
bpy.context.scene.BCFProperties.bcf_file = filepath
|
||||
|
||||
@classmethod
|
||||
def set_by_filepath(cls, filepath):
|
||||
cls.set(None, filepath)
|
||||
|
||||
@classmethod
|
||||
def unload_bcfxml(cls):
|
||||
cls.set(None, "")
|
||||
|
||||
@@ -42,11 +42,9 @@ class NewBcfProject(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BCFProperties.is_loaded = False
|
||||
bcfxml = bcf.v2.bcfxml.BcfXml.create_new("New Project")
|
||||
bcfstore.BcfStore.set(bcfxml)
|
||||
bcfstore.BcfStore.set(bcfxml, "")
|
||||
bpy.ops.bim.load_bcf_project()
|
||||
context.scene.BCFProperties.is_loaded = True
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -59,11 +57,10 @@ class LoadBcfProject(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
if self.filepath:
|
||||
bcfstore.BcfStore.bcfxml = bcf.bcfxml.load(self.filepath)
|
||||
bcfstore.BcfStore.set_by_filepath(self.filepath)
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
context.scene.BCFProperties.name = bcfxml.project.name
|
||||
bpy.ops.bim.load_bcf_topics()
|
||||
context.scene.BCFProperties.is_loaded = True
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -77,8 +74,7 @@ class UnloadBcfProject(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bcfstore.BcfStore.set(None)
|
||||
context.scene.BCFProperties.is_loaded = False
|
||||
bcfstore.BcfStore.unload_bcfxml()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -267,6 +263,7 @@ class SaveBcfProject(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
bcfxml.save(self.filepath)
|
||||
bcfstore.BcfStore.set(bcfxml, self.filepath)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -964,7 +961,8 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
context_override = {}
|
||||
context_override["object"] = context_override["active_object"] = objs[0]
|
||||
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
|
||||
bpy.ops.object.hide_view_set(context_override, unselected=True)
|
||||
with context.temp_override(**context_override):
|
||||
bpy.ops.object.hide_view_set(unselected=True)
|
||||
context.area.type = old
|
||||
|
||||
if viewpoint.visualization_info.components.view_setup_hints:
|
||||
@@ -978,14 +976,16 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
self.hide_spaces(context)
|
||||
self.set_openings_visibility(False, context)
|
||||
|
||||
# set selection at the end not to conflict with .hide_spaces
|
||||
self.set_selection(viewpoint)
|
||||
self.set_colours(viewpoint)
|
||||
|
||||
def hide_spaces(self, context):
|
||||
old = context.area.type
|
||||
context.area.type = "VIEW_3D"
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bpy.ops.object.select_pattern(pattern="IfcSpace/*")
|
||||
bpy.ops.object.hide_view_set({})
|
||||
bpy.ops.object.hide_view_set()
|
||||
context.area.type = old
|
||||
|
||||
def set_openings_visibility(self, is_visible, context):
|
||||
|
||||
@@ -41,37 +41,37 @@ def purge():
|
||||
|
||||
|
||||
def updateBcfReferenceLink(self, context):
|
||||
if context.scene.BCFProperties.is_loaded:
|
||||
if bcfstore.BcfStore.get_bcfxml():
|
||||
bpy.ops.bim.edit_bcf_reference_links()
|
||||
|
||||
|
||||
def updateBcfLabel(self, context):
|
||||
if context.scene.BCFProperties.is_loaded:
|
||||
if bcfstore.BcfStore.get_bcfxml():
|
||||
bpy.ops.bim.edit_bcf_labels()
|
||||
|
||||
|
||||
def updateBcfProjectName(self, context):
|
||||
if self.is_loaded:
|
||||
if bcfstore.BcfStore.get_bcfxml():
|
||||
bpy.ops.bim.edit_bcf_project_name()
|
||||
|
||||
|
||||
def updateBcfAuthor(self, context):
|
||||
if self.is_loaded:
|
||||
if bcfstore.BcfStore.get_bcfxml():
|
||||
bpy.ops.bim.edit_bcf_author()
|
||||
|
||||
|
||||
def updateBcfTopicName(self, context):
|
||||
if context.scene.BCFProperties.is_loaded:
|
||||
if bcfstore.BcfStore.get_bcfxml():
|
||||
bpy.ops.bim.edit_bcf_topic_name()
|
||||
|
||||
|
||||
def updateBcfTopicIsEditable(self, context):
|
||||
if context.scene.BCFProperties.is_loaded and not self.is_editable:
|
||||
if bcfstore.BcfStore.get_bcfxml() and not self.is_editable:
|
||||
bpy.ops.bim.edit_bcf_topic()
|
||||
|
||||
|
||||
def updateBcfCommentIsEditable(self, context):
|
||||
if context.scene.BCFProperties.is_loaded and not self.is_editable:
|
||||
if bcfstore.BcfStore.get_bcfxml() and not self.is_editable:
|
||||
bpy.ops.bim.edit_bcf_comment(comment_guid=self.name)
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ class BcfTopic(PropertyGroup):
|
||||
|
||||
|
||||
class BCFProperties(PropertyGroup):
|
||||
is_loaded: BoolProperty(name="Is Loaded", default=False)
|
||||
bcf_file: StringProperty(name="BCF File")
|
||||
comment_text_width: IntProperty(name="Comment Text Width", default=40)
|
||||
name: StringProperty(default="", name="Project Name", update=updateBcfProjectName)
|
||||
author: StringProperty(default="john@doe.com", name="Author Email", update=updateBcfAuthor)
|
||||
|
||||
@@ -39,8 +39,7 @@ class BIM_PT_bcf(Panel):
|
||||
scene = context.scene
|
||||
props = scene.BCFProperties
|
||||
|
||||
|
||||
if not props.is_loaded:
|
||||
if not bcfstore.BcfStore.get_bcfxml():
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.new_bcf_project", text="New Project")
|
||||
row.operator("bim.load_bcf_project", text="Load Project")
|
||||
|
||||
@@ -62,14 +62,14 @@ class BoundaryDecorator:
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
|
||||
@@ -63,7 +63,8 @@ def disable_editing_boundary_geometry(context):
|
||||
|
||||
|
||||
class Loader:
|
||||
def __init__(self):
|
||||
def __init__(self, operator=None):
|
||||
self.operator = operator
|
||||
self.ifc_file = None
|
||||
self.logger = None
|
||||
self.ifc_importer = None
|
||||
@@ -85,14 +86,33 @@ class Loader:
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
except RuntimeError:
|
||||
# Fallback solution for invalid geometry provided by Revit. (InnerBoundaries cuting OuterBoundary)
|
||||
print(f"Failed to create mesh from IfcRelSpaceBoundary with ID {boundary.id()}. Geometry might be invalid")
|
||||
shape = ifcopenshell.geom.create_shape(self.fallback_settings, surface.OuterBoundary)
|
||||
self.operator.report(
|
||||
{"WARNING"},
|
||||
(
|
||||
f"IfcRelSpaceBoundary {boundary.id()} mesh creation failed. Geometry might be invalid. Using fallback solution"
|
||||
),
|
||||
)
|
||||
try:
|
||||
shape = ifcopenshell.geom.create_shape(self.fallback_settings, surface.OuterBoundary)
|
||||
except RuntimeError:
|
||||
self.operator.report(
|
||||
{"WARNING"},
|
||||
f"Skipping IfcRelSpaceBoundary {boundary.id()}. Fallback solution failed. Geometry might be invalid",
|
||||
)
|
||||
return None
|
||||
mesh = bpy.data.meshes.new(str(surface.id()))
|
||||
bm = bmesh.new()
|
||||
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
|
||||
bm.faces.new(verts)
|
||||
for inner_boundary in surface.InnerBoundaries:
|
||||
shape = ifcopenshell.geom.create_shape(self.fallback_settings, inner_boundary)
|
||||
try:
|
||||
shape = ifcopenshell.geom.create_shape(self.fallback_settings, inner_boundary)
|
||||
except RuntimeError:
|
||||
self.operator.report(
|
||||
{"WARNING"},
|
||||
f"Skipping an inner boundary for IfcRelSpaceBoundary with ID {boundary.id()}. Geometry might be invalid",
|
||||
)
|
||||
return None
|
||||
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
|
||||
for i in range(len(verts) - 1):
|
||||
bm.edges.new(verts[i : i + 2])
|
||||
@@ -103,7 +123,7 @@ class Loader:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
matrix = mathutils.Matrix(
|
||||
ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist()
|
||||
)
|
||||
)
|
||||
matrix.translation *= unit_scale
|
||||
mesh.transform(matrix)
|
||||
return mesh
|
||||
@@ -126,7 +146,15 @@ class Loader:
|
||||
self.ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
|
||||
self.ifc_importer.file = self.ifc_file
|
||||
|
||||
def load_boundary(self, boundary, blender_space):
|
||||
def load_boundary(self, boundary, blender_space=None):
|
||||
if not blender_space:
|
||||
if not boundary.RelatingSpace:
|
||||
self.operator.report(
|
||||
{"WARNING"},
|
||||
f"Skipping IfcRelSpaceBoundary {boundary.id()} which have no relating space. Invalid boundary.",
|
||||
)
|
||||
return
|
||||
blender_space = tool.Ifc.get_object(boundary.RelatingSpace)
|
||||
obj = tool.Ifc.get_object(boundary)
|
||||
if obj:
|
||||
return obj
|
||||
@@ -145,9 +173,9 @@ class LoadProjectSpaceBoundaries(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
loader = Loader()
|
||||
loader = Loader(self)
|
||||
for rel in tool.Ifc.get().by_type("IfcRelSpaceBoundary"):
|
||||
loader.load_boundary(rel, tool.Ifc.get_object(rel.RelatingSpace))
|
||||
loader.load_boundary(rel)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -158,7 +186,7 @@ class LoadBoundary(bpy.types.Operator):
|
||||
boundary_id: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
loader = Loader()
|
||||
loader = Loader(self)
|
||||
for obj in context.visible_objects:
|
||||
obj.select_set(False)
|
||||
obj = loader.load_boundary(tool.Ifc.get().by_id(self.boundary_id), context.active_object)
|
||||
@@ -173,7 +201,7 @@ class LoadSpaceBoundaries(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
loader = Loader()
|
||||
loader = Loader(self)
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
for rel in element.BoundedBy or []:
|
||||
loader.load_boundary(rel, context.active_object)
|
||||
@@ -421,7 +449,7 @@ class EnableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context))
|
||||
if not bpy.app.background:
|
||||
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -459,7 +487,7 @@ class EditBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_surface)
|
||||
|
||||
old_mesh = obj.data
|
||||
loader = Loader()
|
||||
loader = Loader(self)
|
||||
obj.data = loader.create_mesh(element)
|
||||
tool.Geometry.delete_data(old_mesh)
|
||||
|
||||
@@ -484,7 +512,7 @@ class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMBoundaryProperties
|
||||
loader = Loader()
|
||||
loader = Loader(self)
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not getattr(element, "BoundedBy", None):
|
||||
@@ -538,6 +566,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
parent_boundaries = []
|
||||
|
||||
objs = context.selected_objects
|
||||
if len(objs) == 2:
|
||||
@@ -552,8 +581,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
related_building_element = element
|
||||
related_building_element_obj = obj
|
||||
parent_boundary = self.create_element_boundary(
|
||||
context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
||||
)
|
||||
if parent_boundary:
|
||||
parent_boundaries.append(parent_boundary)
|
||||
elif len(objs) == 1:
|
||||
# Optionally the user may select just the space, and the building element shall be auto-detected
|
||||
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY")
|
||||
@@ -563,8 +598,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
relating_space = element
|
||||
relating_space_obj = objs[0]
|
||||
|
||||
target = bpy.context.scene.cursor.location
|
||||
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
if not collection_obj:
|
||||
@@ -576,19 +609,37 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
|
||||
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
|
||||
if not (subelement.is_a("IfcWall") or subelement.is_a("IfcSlab")):
|
||||
if not (
|
||||
subelement.is_a("IfcWall") or subelement.is_a("IfcSlab") or subelement.is_a("IfcVirtualElement")
|
||||
):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(subelement)
|
||||
if obj:
|
||||
raycast = obj.closest_point_on_mesh(obj.matrix_world.inverted() @ target, distance=0.1)
|
||||
if raycast[0]:
|
||||
related_building_element = subelement
|
||||
related_building_element_obj = obj
|
||||
break
|
||||
related_building_element = subelement
|
||||
related_building_element_obj = obj
|
||||
parent_boundary = self.create_element_boundary(
|
||||
context,
|
||||
relating_space,
|
||||
relating_space_obj,
|
||||
related_building_element,
|
||||
related_building_element_obj,
|
||||
)
|
||||
if parent_boundary:
|
||||
parent_boundaries.append(parent_boundary)
|
||||
|
||||
bpy.ops.bim.show_boundaries()
|
||||
for parent_boundary in parent_boundaries:
|
||||
obj = tool.Ifc.get_object(parent_boundary)
|
||||
obj.select_set(True)
|
||||
|
||||
def create_element_boundary(
|
||||
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
||||
):
|
||||
if not relating_space or not related_building_element:
|
||||
return
|
||||
|
||||
# Find which face on space should be bounded to related building element
|
||||
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(relating_space_obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
|
||||
@@ -609,8 +660,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not target_face:
|
||||
return
|
||||
|
||||
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
|
||||
|
||||
# Is this right? Or should I use loop?
|
||||
target_face_verts = [v.co.copy() for v in target_face.verts]
|
||||
target_face_matrix = self.get_face_matrix(*[v.copy() for v in target_face_verts[0:3]])
|
||||
@@ -630,6 +679,18 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
gross_boundary_polygon = geom
|
||||
break
|
||||
|
||||
if (
|
||||
not (isinstance(gross_boundary_polygon, shapely.Polygon) and gross_boundary_polygon.is_valid)
|
||||
or gross_boundary_polygon.is_empty
|
||||
):
|
||||
return
|
||||
|
||||
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
|
||||
# internal space
|
||||
parent_boundary.InternalOrExternalBoundary = "EXTERNAL"
|
||||
|
||||
# The gross boundary polygon may not be a true gross boundary since it
|
||||
# may have openings already removed, such as in IFC4 Reference View. So
|
||||
# we cheat by using the exterior boundary to mean "gross". Later, we
|
||||
@@ -637,19 +698,25 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# space.
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
net_boundary_polygon = shapely.Polygon(gross_boundary_polygon)
|
||||
|
||||
inner_boundaries = []
|
||||
|
||||
for rel in getattr(related_building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
filling = None
|
||||
if opening.HasFillings:
|
||||
filling = opening.HasFillings[0].RelatedBuildingElement
|
||||
if not opening.HasFillings:
|
||||
continue
|
||||
# TODO: HasFilling is a zero to many relationship. How to handle many ?
|
||||
filling = opening.HasFillings[0].RelatedBuildingElement
|
||||
|
||||
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
|
||||
|
||||
net_boundary_polygon = net_boundary_polygon.difference(opening_polygon)
|
||||
# An inner boundary is supposed to overlap its parent boundary according to IFC4 documentation
|
||||
# so we extend our exterior boundary with all opening which has a filling
|
||||
# Also see Implementation aggreement SB 1.1 for IFC 2x3 TC1 Space Boundary Addon View
|
||||
# https://standards.buildingsmart.org/MVD/RELEASE/IFC2x3/TC1/SB1_1/IFC%20Space%20Boundary%20Implementation%20Agreement%20Addendum%202010-03-22.pdf
|
||||
unionised_object = exterior_boundary_polygon.union(opening_polygon)
|
||||
if isinstance(unionised_object, shapely.Polygon):
|
||||
exterior_boundary_polygon = unionised_object
|
||||
|
||||
# Only openings that are projected onto our exterior boundary are relevant.
|
||||
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
|
||||
continue
|
||||
@@ -657,24 +724,25 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
|
||||
boundary.RelatingSpace = relating_space
|
||||
boundary.RelatedBuildingElement = filling or related_building_element
|
||||
boundary.RelatedBuildingElement = filling
|
||||
boundary.ConnectionGeometry = connection_geometry
|
||||
boundary.PhysicalOrVirtualBoundary = "PHYSICAL" if filling else "VIRTUAL"
|
||||
boundary.InternalOrExternalBoundary = "INTERNAL"
|
||||
|
||||
if related_building_element.is_a("IfcVirtualElement"):
|
||||
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
else:
|
||||
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
boundary.InternalOrExternalBoundary = parent_boundary.InternalOrExternalBoundary
|
||||
self.set_boundary_name(boundary)
|
||||
if boundary.is_a("IfcRelSpaceBoundary2ndLevel"):
|
||||
boundary.ParentBoundary = parent_boundary
|
||||
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(net_boundary_polygon, target_face_matrix)
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(
|
||||
exterior_boundary_polygon, target_face_matrix
|
||||
)
|
||||
parent_boundary.RelatingSpace = relating_space
|
||||
parent_boundary.RelatedBuildingElement = related_building_element
|
||||
parent_boundary.ConnectionGeometry = connection_geometry
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
|
||||
|
||||
bpy.ops.bim.show_boundaries()
|
||||
obj = tool.Ifc.get_object(parent_boundary)
|
||||
obj.select_set(True)
|
||||
self.set_boundary_name(parent_boundary)
|
||||
return parent_boundary
|
||||
|
||||
def get_face_matrix(self, p1, p2, p3):
|
||||
edge1 = p2 - p1
|
||||
@@ -729,11 +797,13 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Model.unit_scale = self.unit_scale
|
||||
|
||||
surface = tool.Ifc.get().createIfcCurveBoundedPlane()
|
||||
surface.BasisSurface = tool.Ifc.get().createIfcPlane(tool.Ifc.get().createIfcAxis2Placement3D(
|
||||
tool.Ifc.get().createIfcCartesianPoint([o / self.unit_scale for o in p1]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]),
|
||||
))
|
||||
surface.BasisSurface = tool.Ifc.get().createIfcPlane(
|
||||
tool.Ifc.get().createIfcAxis2Placement3D(
|
||||
tool.Ifc.get().createIfcCartesianPoint([o / self.unit_scale for o in p1]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]),
|
||||
)
|
||||
)
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in polygon.exterior.coords]
|
||||
@@ -746,9 +816,24 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
inner_boundaries.append(tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False))
|
||||
else:
|
||||
pass # TODO
|
||||
pass # TODO
|
||||
|
||||
surface.OuterBoundary = outer_boundary
|
||||
surface.InnerBoundaries = inner_boundaries
|
||||
|
||||
return surface
|
||||
|
||||
def set_boundary_name(self, boundary):
|
||||
"""
|
||||
By convention 1stLevel and 2ndLevel boundary have specific name and description
|
||||
See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRelSpaceBoundary.htm
|
||||
Warning: IfcRelSpaceBoundary2ndLevel is a subclass of IfcRelSpaceBoundary1stLevel test order matter
|
||||
"""
|
||||
if boundary.is_a("IfcRelSpaceBoundary2ndLevel"):
|
||||
boundary.Name = "2ndLevel"
|
||||
if boundary.CorrespondingBoundary:
|
||||
boundary.Description = "2a"
|
||||
else:
|
||||
boundary.Description = "2b"
|
||||
elif boundary.is_a("IfcRelSpaceBoundary1stLevel"):
|
||||
boundary.Name = "1stLevel"
|
||||
|
||||
@@ -33,7 +33,13 @@ class LoadBSDDDomains(bpy.types.Operator):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
props.domains.clear()
|
||||
client = bsdd.Client()
|
||||
for domain in sorted(client.Domain(), key=lambda x: x["name"]):
|
||||
|
||||
if props.load_preview_domains:
|
||||
domains = client.Domain()
|
||||
else:
|
||||
domains = [d for d in client.Domain() if d["status"] == "Active"]
|
||||
|
||||
for domain in sorted(domains, key=lambda x: x["name"]):
|
||||
new = props.domains.add()
|
||||
new.name = domain["name"]
|
||||
new.namespace_uri = domain["namespaceUri"]
|
||||
@@ -119,14 +125,23 @@ class GetBSDDClassificationProperties(bpy.types.Operator):
|
||||
possible_values = prop.get("possibleValues", []) or []
|
||||
possible_values = [v["value"] for v in possible_values]
|
||||
|
||||
psets[pset][prop["name"]] = possible_values
|
||||
psets[pset][prop["name"]] = {"data_type": prop["dataType"], "possible_values": possible_values}
|
||||
|
||||
data_type_map = {
|
||||
"String": "string",
|
||||
"Real": "float",
|
||||
"Boolean": "boolean",
|
||||
}
|
||||
|
||||
for pset_name, pset in psets.items():
|
||||
new = bprops.classification_psets.add()
|
||||
new.name = pset_name
|
||||
for name, values in pset.items():
|
||||
for name, data in pset.items():
|
||||
new2 = new.properties.add()
|
||||
new2.name = name
|
||||
new2.enum_items = json.dumps(values)
|
||||
new2.data_type = "enum"
|
||||
if data["possible_values"]:
|
||||
new2.enum_items = json.dumps(data["possible_values"])
|
||||
new2.data_type = "enum"
|
||||
else:
|
||||
new2.data_type = data_type_map[data["data_type"]]
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -63,4 +63,5 @@ class BIMBSDDProperties(PropertyGroup):
|
||||
active_classification_index: IntProperty(name="Active Classification Index")
|
||||
keyword: StringProperty(name="Keyword")
|
||||
should_filter_ifc_class: BoolProperty(name="Filter Active IFC Class", default=True)
|
||||
load_preview_domains: BoolProperty(name="Load Preview Domains", default=False)
|
||||
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
|
||||
|
||||
@@ -32,6 +32,11 @@ class BIM_PT_bsdd(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "load_preview_domains")
|
||||
if len(props.domains):
|
||||
row.operator("bim.load_bsdd_domains", text="", icon="FILE_REFRESH")
|
||||
|
||||
if props.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + props.active_domain, icon="URL")
|
||||
@@ -57,7 +62,10 @@ class BIM_UL_bsdd_domains(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner})")
|
||||
if item.status != "Active":
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR")
|
||||
else:
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner})")
|
||||
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.name = item.name
|
||||
op.uri = item.namespace_uri
|
||||
|
||||
@@ -24,6 +24,8 @@ import bpy_extras
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector, Matrix
|
||||
from math import pi, radians, sin, cos, sqrt
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
|
||||
messages = {
|
||||
"SHARED_VERTEX": "Shared Vertex, no intersection possible",
|
||||
@@ -140,6 +142,9 @@ class CadFillet(bpy.types.Operator):
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
@@ -361,6 +366,10 @@ class CadOffset(bpy.types.Operator):
|
||||
# dialog that Mesh Tools has. Sverchok is 2D based, but has a weird side
|
||||
# effect of converting an unclosed loop into a closed loop which is also
|
||||
# not what users expect.
|
||||
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.distance = self.distance * si_conversion
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
@@ -498,6 +507,8 @@ class CadOffset(bpy.types.Operator):
|
||||
local_direction = (rotation_i @ direction).normalized()
|
||||
normals.append(local_direction)
|
||||
|
||||
|
||||
|
||||
if len(normals) == 2:
|
||||
# https://stackoverflow.com/a/54042831/9627415
|
||||
new_normal = (normals[0].lerp(normals[1], 0.5)).normalized()
|
||||
@@ -531,7 +542,7 @@ class CadOffset(bpy.types.Operator):
|
||||
class AddIfcCircle(bpy.types.Operator):
|
||||
bl_idname = "bim.add_ifccircle"
|
||||
bl_label = "Add IfcCircle"
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.5)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -539,6 +550,9 @@ class AddIfcCircle(bpy.types.Operator):
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
if self.has_selected_existing_circle(context):
|
||||
self.change_radius(context)
|
||||
else:
|
||||
@@ -622,6 +636,9 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
if self.has_selected_existing_arc(context):
|
||||
self.change_radius(context)
|
||||
else:
|
||||
@@ -786,8 +803,8 @@ class AlignViewToProfile(bpy.types.Operator):
|
||||
class AddRectangle(bpy.types.Operator):
|
||||
bl_idname = "bim.add_rectangle"
|
||||
bl_label = "Add Rectangle"
|
||||
x: bpy.props.FloatProperty(name="X", default=0.1)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1)
|
||||
x: bpy.props.FloatProperty(name="X", default=1)
|
||||
y: bpy.props.FloatProperty(name="Y", default=1)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -795,6 +812,10 @@ class AddRectangle(bpy.types.Operator):
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.x = self.x * si_conversion
|
||||
self.y = self.y * si_conversion
|
||||
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
|
||||
|
||||
@@ -20,26 +20,33 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.AddClashSet,
|
||||
operator.AddClashSource,
|
||||
operator.ExecuteIfcClash,
|
||||
operator.ExportClashSets,
|
||||
operator.ImportClashSets,
|
||||
operator.AddClashSet,
|
||||
operator.LoadIfcClashes,
|
||||
operator.SaveIfcClashes,
|
||||
operator.LoadSmartGroupsForActiveClashSet,
|
||||
operator.RemoveClashSet,
|
||||
operator.AddClashSource,
|
||||
operator.RemoveClashSource,
|
||||
operator.SelectClashSource,
|
||||
operator.ExecuteIfcClash,
|
||||
operator.SelectIfcClashResults,
|
||||
operator.SelectClash,
|
||||
operator.SelectClashResults,
|
||||
operator.SelectClashSource,
|
||||
operator.SelectIfcClashResults,
|
||||
operator.SelectSmartGroup,
|
||||
operator.SelectSmartGroupedClashesPath,
|
||||
operator.SmartClashGroup,
|
||||
operator.SelectSmartGroup,
|
||||
operator.LoadSmartGroupsForActiveClashSet,
|
||||
prop.Clash,
|
||||
prop.ClashSource,
|
||||
prop.ClashSet,
|
||||
prop.SmartClashGroup,
|
||||
prop.BIMClashProperties,
|
||||
ui.BIM_PT_ifcclash,
|
||||
ui.BIM_PT_clash_manager,
|
||||
ui.BIM_PT_dumb_clash_manager,
|
||||
ui.BIM_PT_smart_clash_manager,
|
||||
ui.BIM_UL_clashes,
|
||||
ui.BIM_UL_clash_sets,
|
||||
ui.BIM_UL_smart_groups,
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ import ifcopenshell
|
||||
from mathutils import Matrix, Vector
|
||||
from math import radians
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class ExportClashSets(bpy.types.Operator):
|
||||
@@ -245,12 +246,14 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
y = Vector([y.x, y.y, y.z])
|
||||
x = y.cross(z)
|
||||
|
||||
mat = Matrix([
|
||||
[x[0], y[0], z[0], p.x],
|
||||
[x[1], y[1], z[1], p.y],
|
||||
[x[2], y[2], z[2], p.z],
|
||||
[0, 0, 0, 0],
|
||||
])
|
||||
mat = Matrix(
|
||||
[
|
||||
[x[0], y[0], z[0], p.x],
|
||||
[x[1], y[1], z[1], p.y],
|
||||
[x[2], y[2], z[2], p.z],
|
||||
[0, 0, 0, 0],
|
||||
]
|
||||
)
|
||||
|
||||
camera.matrix_world = mat
|
||||
context.scene.camera = camera
|
||||
@@ -349,6 +352,119 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadIfcClashes(bpy.types.Operator):
|
||||
bl_idname = "bim.load_ifc_clashes"
|
||||
bl_label = "Load IFC Clashes"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Load the clashing IFC geometry stored in a file"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
with open(self.filepath) as f:
|
||||
clash_sets_json = json.load(f)
|
||||
active_clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
|
||||
for clash_set in clash_sets_json:
|
||||
if not "clashes" in clash_set.keys():
|
||||
self.report({"WARNING"}, "No clashes found for the selected Clash Set.")
|
||||
return {"CANCELLED"}
|
||||
active_clash_set.clashes.clear()
|
||||
for clash in clash_set["clashes"].values():
|
||||
blender_clash = 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"]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SaveIfcClashes(bpy.types.Operator):
|
||||
bl_idname = "bim.save_ifc_clashes"
|
||||
bl_label = "Save IFC Clashes"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Save the clashing IFC geometry stored in a file"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
clash_sets_json = self.load_json()
|
||||
active_clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
self.update_clash_sets(clash_sets_json, active_clash_set)
|
||||
self.save_json(clash_sets_json)
|
||||
return {"FINISHED"}
|
||||
|
||||
def load_json(self):
|
||||
with open(self.filepath) as f:
|
||||
return json.load(f)
|
||||
|
||||
def update_clash_sets(self, clash_sets_json, active_clash_set):
|
||||
for clash_set in clash_sets_json:
|
||||
if clash_set["name"] != active_clash_set.name or "clashes" not in clash_set.keys():
|
||||
continue
|
||||
for clash in active_clash_set.clashes:
|
||||
clash_key = clash.a_global_id + "-" + clash.b_global_id
|
||||
if clash_set.get("clashes", {}).get(clash_key, None) is not None:
|
||||
clash_set["clashes"][clash_key]["status"] = clash.status
|
||||
|
||||
def save_json(self, clash_sets_json):
|
||||
with open(self.filepath, "w") as destination:
|
||||
json.dump(clash_sets_json, destination, indent=4)
|
||||
|
||||
|
||||
class SelectClash(bpy.types.Operator):
|
||||
bl_idname = "bim.select_clash"
|
||||
bl_label = "Select Clash"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select the clashing IFC geometry stored in a file"
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
clash = context.scene.BIMClashProperties.active_clash
|
||||
|
||||
products = []
|
||||
linked_objects = []
|
||||
try:
|
||||
products.append(tool.Ifc.get().by_guid(clash.a_global_id))
|
||||
except:
|
||||
linked_objects.append(clash.a_name)
|
||||
try:
|
||||
products.append(tool.Ifc.get().by_guid(clash.b_global_id))
|
||||
except:
|
||||
linked_objects.append(clash.b_name)
|
||||
|
||||
tool.Spatial.select_products(products, unhide=True)
|
||||
print(linked_objects)
|
||||
for obj_name in linked_objects:
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
else:
|
||||
print("Object not found:", obj_name)
|
||||
if context.scene.BIMClashProperties.sould_focus_on_clash:
|
||||
context_override = tool.Blender.get_viewport_context()
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.view3d.view_selected()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SmartClashGroup(bpy.types.Operator):
|
||||
bl_idname = "bim.smart_clash_group"
|
||||
bl_label = "Smart Group Clashes"
|
||||
@@ -433,9 +549,9 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
|
||||
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
|
||||
new_group.number = f"{smart_group}"
|
||||
for pair in global_id_pairs:
|
||||
for id in pair:
|
||||
for guid in pair:
|
||||
new_global_id = new_group.global_ids.add()
|
||||
new_global_id.name = id
|
||||
new_global_id.guid = guid
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -450,20 +566,16 @@ class SelectSmartGroup(bpy.types.Operator):
|
||||
return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
# Select smart group in view
|
||||
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
|
||||
# print(selected_smart_group.number)
|
||||
|
||||
for obj in context.visible_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
products = []
|
||||
for global_id in selected_smart_group.global_ids:
|
||||
try:
|
||||
products.append(tool.Ifc.get().by_guid(global_id.guid))
|
||||
except:
|
||||
continue
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
for id in selected_smart_group.global_ids:
|
||||
# print("Id: ", id)
|
||||
# print("Global id: ", element.GlobalId)
|
||||
if element.GlobalId in id.name:
|
||||
# print("object match: ", global_id)
|
||||
obj.select_set(True)
|
||||
|
||||
tool.Spatial.select_products(products, unhide=True)
|
||||
context_override = tool.Blender.get_viewport_context()
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.view3d.view_selected()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -43,11 +43,20 @@ class ClashSource(PropertyGroup):
|
||||
)
|
||||
|
||||
|
||||
class Clash(PropertyGroup):
|
||||
a_global_id: StringProperty(name="A")
|
||||
b_global_id: StringProperty(name="B")
|
||||
a_name: StringProperty(name="A Name")
|
||||
b_name: StringProperty(name="B Name")
|
||||
status: BoolProperty(name="Status", default=False)
|
||||
|
||||
|
||||
class ClashSet(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
tolerance: FloatProperty(name="Tolerance")
|
||||
a: CollectionProperty(name="Group A", type=ClashSource)
|
||||
b: CollectionProperty(name="Group B", type=ClashSource)
|
||||
clashes: CollectionProperty(name="Clashes", type=Clash)
|
||||
|
||||
|
||||
class SmartClashGroup(PropertyGroup):
|
||||
@@ -63,11 +72,13 @@ class BIMClashProperties(PropertyGroup):
|
||||
clash_results_path: StringProperty(name="Clash Results Path")
|
||||
smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path")
|
||||
active_clash_set_index: IntProperty(name="Active Clash Set Index")
|
||||
active_clash_index: IntProperty(name="Active Clash Index")
|
||||
smart_clash_groups: CollectionProperty(name="Smart Clash Groups", type=SmartClashGroup)
|
||||
active_smart_group_index: IntProperty(name="Active Smart Group Index")
|
||||
smart_clash_grouping_max_distance: IntProperty(
|
||||
name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10
|
||||
)
|
||||
sould_focus_on_clash: BoolProperty(name="Show Focus Clash", default=False)
|
||||
|
||||
@property
|
||||
def active_clash_set(self):
|
||||
@@ -80,3 +91,9 @@ class BIMClashProperties(PropertyGroup):
|
||||
if not self.smart_clash_groups:
|
||||
return None
|
||||
return self.smart_clash_groups[self.active_smart_group_index]
|
||||
|
||||
@property
|
||||
def active_clash(self):
|
||||
if not self.active_clash_set:
|
||||
return None
|
||||
return self.active_clash_set.clashes[self.active_clash_index]
|
||||
|
||||
@@ -107,6 +107,42 @@ class BIM_PT_clash_manager(Panel):
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_clash_detection"
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
|
||||
class BIM_PT_dumb_clash_manager(Panel):
|
||||
bl_idname = "BIM_PT_dumb_clash_manager"
|
||||
bl_label = "Dumb Manager"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_clash_manager"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMClashProperties
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.load_ifc_clashes", text="LOAD IFC CLASHES", icon="IMPORT")
|
||||
row.operator("bim.save_ifc_clashes", text="SAVE CLASH RESULTS", icon="EXPORT")
|
||||
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
|
||||
# bim.select_clash
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.select_clash", text="SELECT CLASH", icon="IMPORT")
|
||||
row.prop(props, "sould_focus_on_clash", icon="RESTRICT_VIEW_OFF")
|
||||
|
||||
|
||||
class BIM_PT_smart_clash_manager(Panel):
|
||||
bl_idname = "BIM_PT_smart_clash_manager"
|
||||
bl_label = "Smart Clash Manager"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_clash_manager"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMClashProperties
|
||||
@@ -156,3 +192,14 @@ class BIM_UL_smart_groups(bpy.types.UIList):
|
||||
layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0)
|
||||
else:
|
||||
layout.label(text="", translate=False)
|
||||
|
||||
|
||||
class BIM_UL_clashes(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=str(item.a_name), translate=False, icon="NONE", icon_value=0)
|
||||
row.label(text=str(item.b_name), translate=False, icon="NONE", icon_value=0)
|
||||
row.prop(item, "status", text="")
|
||||
else:
|
||||
layout.label(text="", translate=False)
|
||||
|
||||
@@ -24,12 +24,18 @@ classes = (
|
||||
operator.AddClassificationFromBSDD,
|
||||
operator.AddClassificationReference,
|
||||
operator.AddClassificationReferenceFromBSDD,
|
||||
operator.AddManualClassification,
|
||||
operator.AddManualClassificationReference,
|
||||
operator.ChangeClassificationLevel,
|
||||
operator.DisableAddingManualClassification,
|
||||
operator.DisableAddingManualClassificationReference,
|
||||
operator.DisableEditingClassification,
|
||||
operator.DisableEditingClassificationReference,
|
||||
operator.DisableEditingClassificationReferences,
|
||||
operator.EditClassification,
|
||||
operator.EditClassificationReference,
|
||||
operator.EnableAddingManualClassification,
|
||||
operator.EnableAddingManualClassificationReference,
|
||||
operator.EnableEditingClassification,
|
||||
operator.EnableEditingClassificationReference,
|
||||
operator.LoadClassificationLibrary,
|
||||
|
||||
@@ -73,6 +73,10 @@ class ReferencesData:
|
||||
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 = {}
|
||||
@@ -83,6 +87,8 @@ class ClassificationReferencesData(ReferencesData):
|
||||
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):
|
||||
@@ -105,6 +111,8 @@ class MaterialClassificationsData(ReferencesData):
|
||||
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):
|
||||
@@ -127,6 +135,8 @@ class CostClassificationsData(ReferencesData):
|
||||
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):
|
||||
|
||||
@@ -54,6 +54,50 @@ class AddClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
class AddManualClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_manual_classification"
|
||||
bl_label = "Add Manual Classification"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMClassificationProperties
|
||||
attributes = blenderbim.bim.helper.export_attributes(props.classification_attributes)
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification="Unnamed"
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"classification.edit_classification", tool.Ifc.get(), classification=classification, attributes=attributes
|
||||
)
|
||||
props.is_adding = False
|
||||
|
||||
|
||||
class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_manual_classification_reference"
|
||||
bl_label = "Add Manual Classification Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMClassificationReferenceProperties
|
||||
attributes = blenderbim.bim.helper.export_attributes(props.reference_attributes)
|
||||
product = tool.Ifc.get_entity(obj)
|
||||
# TODO: refactor and support material and cost item classifications
|
||||
classification = tool.Ifc.get().by_id(int(props.classifications))
|
||||
reference = ifcopenshell.api.run(
|
||||
"classification.add_reference",
|
||||
tool.Ifc.get(),
|
||||
product=product,
|
||||
classification=classification,
|
||||
identification="X",
|
||||
name="Unnamed",
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
|
||||
)
|
||||
props.is_adding = False
|
||||
|
||||
|
||||
class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_classification_from_bsdd"
|
||||
bl_label = "Add Classification From bSDD"
|
||||
@@ -73,6 +117,57 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
classification.Edition = domain.version
|
||||
|
||||
|
||||
class EnableAddingManualClassification(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_adding_manual_classification"
|
||||
bl_label = "Enable Adding Manual Classification"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMClassificationProperties
|
||||
props.is_adding = True
|
||||
props.active_classification_id = 0
|
||||
props.classification_attributes.clear()
|
||||
blenderbim.bim.helper.import_attributes2("IfcClassification", props.classification_attributes)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableAddingManualClassification(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_adding_manual_classification"
|
||||
bl_label = "Disable Adding Manual Classification"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMClassificationProperties
|
||||
props.is_adding = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableAddingManualClassificationReference(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_adding_manual_classification_reference"
|
||||
bl_label = "Enable Adding Manual Classification Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMClassificationReferenceProperties
|
||||
props.is_adding = True
|
||||
props.reference_attributes.clear()
|
||||
blenderbim.bim.helper.import_attributes2("IfcClassificationReference", props.reference_attributes)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableAddingManualClassificationReference(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_adding_manual_classification_reference"
|
||||
bl_label = "Disable Adding Manual Classification Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMClassificationReferenceProperties
|
||||
props.is_adding = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingClassification(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_classification"
|
||||
bl_label = "Enable Editing Classification"
|
||||
@@ -194,21 +289,23 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
objects = [self.obj]
|
||||
|
||||
identification = tool.Ifc.get().by_id(self.reference)[0]
|
||||
active_reference = tool.Ifc.get().by_id(self.reference)
|
||||
identification = active_reference[1]
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
|
||||
for reference in references:
|
||||
if reference[0] == identification:
|
||||
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,
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -324,17 +421,32 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
reference.Location = bsdd_classification.namespace_uri
|
||||
|
||||
for classification_pset in bprops.classification_psets:
|
||||
pset = ifcopenshell.util.element.get_pset(element, classification_pset.name)
|
||||
is_pset = not classification_pset.name.startswith("Qto_")
|
||||
|
||||
if is_pset:
|
||||
pset = ifcopenshell.util.element.get_pset(element, classification_pset.name, psets_only=True)
|
||||
else:
|
||||
pset = ifcopenshell.util.element.get_pset(element, classification_pset.name, qtos_only=True)
|
||||
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
elif is_pset:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=element, name=classification_pset.name
|
||||
)
|
||||
else:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_qto", tool.Ifc.get(), product=element, name=classification_pset.name
|
||||
)
|
||||
|
||||
properties = {}
|
||||
for prop in classification_pset.properties:
|
||||
properties[prop.name] = prop.get_value()
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=properties)
|
||||
|
||||
if is_pset:
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=properties)
|
||||
else:
|
||||
ifcopenshell.api.run("pset.edit_qto", tool.Ifc.get(), qto=pset, properties=properties)
|
||||
|
||||
|
||||
class ChangeClassificationLevel(bpy.types.Operator):
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim.helper
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.module.classification.data import ClassificationsData
|
||||
from blenderbim.bim.module.classification.data import ClassificationsData, ClassificationReferencesData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -39,6 +40,12 @@ def get_available_classifications(self, context):
|
||||
return ClassificationsData.data["available_classifications"]
|
||||
|
||||
|
||||
def get_classifications(self, context):
|
||||
if not ClassificationReferencesData.is_loaded:
|
||||
ClassificationReferencesData.load()
|
||||
return ClassificationReferencesData.data["classifications"]
|
||||
|
||||
|
||||
class ClassificationReference(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
identification: StringProperty(name="Identification")
|
||||
@@ -48,6 +55,7 @@ class ClassificationReference(PropertyGroup):
|
||||
|
||||
|
||||
class BIMClassificationProperties(PropertyGroup):
|
||||
is_adding: BoolProperty(name="Is Adding", default=False)
|
||||
classification_source: EnumProperty(
|
||||
items=[
|
||||
("FILE", "IFC File", ""),
|
||||
@@ -66,5 +74,7 @@ class BIMClassificationProperties(PropertyGroup):
|
||||
|
||||
|
||||
class BIMClassificationReferenceProperties(PropertyGroup):
|
||||
is_adding: BoolProperty(name="Is Adding", default=False)
|
||||
classifications: EnumProperty(items=get_classifications, name="Classifications")
|
||||
reference_attributes: CollectionProperty(name="Reference Attributes", type=Attribute)
|
||||
active_reference_id: IntProperty(name="Active Reference Id")
|
||||
|
||||
@@ -66,8 +66,14 @@ class BIM_PT_classifications(Panel):
|
||||
self.draw_ui(classification)
|
||||
|
||||
def draw_add_manual_ui(self, context):
|
||||
row = self.layout.row()
|
||||
row.label(text="TODO", icon="ERROR")
|
||||
if self.props.is_adding:
|
||||
blenderbim.bim.helper.draw_attributes(self.props.classification_attributes, self.layout)
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.add_manual_classification", text="Save", icon="CHECKMARK")
|
||||
row.operator("bim.disable_adding_manual_classification", text="", icon="CANCEL")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_adding_manual_classification", text="Add Classification", icon="ADD")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
self.bprops = context.scene.BIMBSDDProperties
|
||||
@@ -144,7 +150,16 @@ class ReferenceUI:
|
||||
|
||||
def draw_add_manual_ui(self, context):
|
||||
row = self.layout.row()
|
||||
row.label(text="TODO", icon="ERROR")
|
||||
row.prop(self.props, "classifications", text="")
|
||||
if self.props.is_adding:
|
||||
blenderbim.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
|
||||
row = self.layout.row(align=True)
|
||||
op = row.operator("bim.add_manual_classification_reference", text="Save", icon="CHECKMARK")
|
||||
op.type = self.data.data["object_type"]
|
||||
row.operator("bim.disable_adding_manual_classification_reference", text="", icon="CANCEL")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
if not self.bprops.active_domain:
|
||||
@@ -239,6 +254,8 @@ class ReferenceUI:
|
||||
name = reference["Identification"] or "No Identification"
|
||||
row.label(text=name, icon="ASSET_MANAGER")
|
||||
row.label(text=reference["Name"] or "")
|
||||
if reference["Location"]:
|
||||
row.operator("bim.open_uri", icon="URL", text="").uri = reference["Location"]
|
||||
if not self.props.active_reference_id:
|
||||
op = row.operator("bim.enable_editing_classification_reference", text="", icon="GREASEPENCIL")
|
||||
op.reference = reference["id"]
|
||||
|
||||
@@ -256,7 +256,7 @@ class CostSchedulesData:
|
||||
for cost_value in cost_item.CostValues or []:
|
||||
label = "{0:.2f}".format(ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value))
|
||||
label += " = {}".format(ifcopenshell.util.cost.serialise_cost_value(cost_value))
|
||||
results.append({"id": cost_value.id(), "label": label})
|
||||
results.append({"id": cost_value.id(), "label": label, "name": cost_value.Name})
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -42,19 +42,21 @@ class BIM_PT_cost_schedules(Panel):
|
||||
CostSchedulesData.load()
|
||||
|
||||
self.props = context.scene.BIMCostProperties
|
||||
row = self.layout.row(align=True)
|
||||
if not self.props.active_cost_schedule_id:
|
||||
row = self.layout.row(align=True)
|
||||
if CostSchedulesData.data["total_cost_schedules"]:
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.export_cost_schedules",icon="EXPORT",text="Export All Schedules")
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT")
|
||||
else:
|
||||
row.label(text="No Cost Schedules found.", icon="TEXT")
|
||||
|
||||
row.operator("bim.add_cost_schedule", icon="ADD", text="")
|
||||
row.operator("import_cost_schedule_csv.bim",icon="IMPORT",text="")
|
||||
|
||||
for schedule in CostSchedulesData.data["schedules"]:
|
||||
self.draw_cost_schedule_ui(schedule)
|
||||
row2 = self.layout.row()
|
||||
row2.alignment = "RIGHT"
|
||||
row2.operator("bim.export_cost_schedules", text="Export all", icon="EXPORT")
|
||||
|
||||
def draw_cost_schedule_ui(self, cost_schedule):
|
||||
row = self.layout.row(align=True)
|
||||
@@ -236,6 +238,8 @@ class BIM_PT_cost_schedules(Panel):
|
||||
blenderbim.bim.helper.draw_attributes(self.props.cost_value_attributes, self.layout.box())
|
||||
|
||||
def draw_readonly_cost_value_ui(self, layout, cost_value):
|
||||
if cost_value["name"]:
|
||||
layout.label(text=f"{cost_value['name']}: ")
|
||||
if self.props.active_cost_value_id == cost_value["id"] and self.props.cost_value_editing_type == "FORMULA":
|
||||
layout.prop(self.props, "cost_value_formula", text="")
|
||||
else:
|
||||
|
||||
@@ -139,6 +139,16 @@ class CoveringToolUI:
|
||||
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
|
||||
@@ -243,7 +253,7 @@ class Hotkey(bpy.types.Operator, Operator):
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
pass
|
||||
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
|
||||
elif tool.Ifc.get_entity(collection_obj):
|
||||
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
|
||||
else:
|
||||
|
||||
@@ -207,6 +207,7 @@ class ExportIfcCsv(bpy.types.Operator):
|
||||
empty=props.empty_value,
|
||||
bool_true=props.true_value,
|
||||
bool_false=props.false_value,
|
||||
concat=props.concat_value,
|
||||
sort=sort,
|
||||
groups=groups,
|
||||
summaries=summaries,
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.module.search.prop import BIMFilterGroup
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from blenderbim.bim.prop import StrProperty, BIMFilterGroup
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -72,6 +71,7 @@ class CsvProperties(PropertyGroup):
|
||||
empty_value: StringProperty(default="-", name="Empty String Value")
|
||||
true_value: StringProperty(default="YES", name="True Value")
|
||||
false_value: StringProperty(default="NO", name="False Value")
|
||||
concat_value: StringProperty(default=", ", name="Concat Value")
|
||||
csv_delimiter: EnumProperty(
|
||||
items=[
|
||||
(";", ";", ""),
|
||||
|
||||
@@ -79,9 +79,11 @@ class BIM_PT_ifccsv(Panel):
|
||||
row.prop(props, "true_value")
|
||||
row = layout.row()
|
||||
row.prop(props, "false_value")
|
||||
row = layout.row()
|
||||
row.prop(props, "concat_value")
|
||||
layout.use_property_split = False
|
||||
|
||||
blenderbim.bim.helper.draw_filter(self.layout, props, SearchData, "csv")
|
||||
blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, SearchData, "csv")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_csv_attribute", icon="ADD")
|
||||
|
||||
@@ -146,16 +146,33 @@ class ValidateIfcFile(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
return tool.Ifc.get()
|
||||
|
||||
def execute(self, context):
|
||||
import ifcopenshell.validate
|
||||
|
||||
class LogDetectionHandler(logging.Handler):
|
||||
message_logged = False
|
||||
|
||||
def emit(self, record):
|
||||
if not self.message_logged:
|
||||
self.message_logged = True
|
||||
|
||||
logger = logging.getLogger("validate")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
ifcopenshell.validate.validate(IfcStore.get_file(), logger, express_rules=True)
|
||||
|
||||
self.report({"INFO"}, "Check validation results in the system console.")
|
||||
# use LogDetectionHandler to check whether there were any validation errors
|
||||
# during `ifcopenshell.validate.validate`
|
||||
handler = LogDetectionHandler()
|
||||
logger.addHandler(handler)
|
||||
ifcopenshell.validate.validate(tool.Ifc.get(), logger, express_rules=True)
|
||||
logger.removeHandler(handler)
|
||||
|
||||
if handler.message_logged:
|
||||
self.report({"INFO"}, "Check validation results in the system console.")
|
||||
else:
|
||||
self.report({"INFO"}, "No validation issues found.")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -458,7 +475,7 @@ class OverrideDisplayType(bpy.types.Operator):
|
||||
|
||||
class PrintUnusedElementStats(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.print_unused_elements_stats"
|
||||
bl_label = "Purge Unused Elements Stats"
|
||||
bl_label = "Print Unused Elements Stats"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Print all unused elements in current IFC project in system console, not limited to the selected class"
|
||||
@@ -467,6 +484,7 @@ class PrintUnusedElementStats(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ignore_contexts: bpy.props.BoolProperty(name="Ignore Contexts", default=True)
|
||||
ignore_relationships: bpy.props.BoolProperty(name="Ignore Relationships", default=True)
|
||||
ignore_types: bpy.props.BoolProperty(name="Ignore Types", default=True)
|
||||
ignore_styled_items: bpy.props.BoolProperty(name="Ignoer Styled Items", default=True)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
@@ -478,6 +496,8 @@ class PrintUnusedElementStats(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ignore_classes += ["IfcRelationship"]
|
||||
if self.ignore_types:
|
||||
ignore_classes += ["IfcTypeProduct"]
|
||||
if self.ignore_styled_items:
|
||||
ignore_classes += ["IfcStyledItem"]
|
||||
|
||||
unused_elements = tool.Debug.print_unused_elements_stats(props.ifc_class_purge, ignore_classes)
|
||||
self.report({"INFO"}, f"{unused_elements} unused elements found, check the system console for the details.")
|
||||
@@ -487,7 +507,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.purge_unused_elements_by_class"
|
||||
bl_label = "Purge Unused Elements By Class"
|
||||
bl_description = (
|
||||
"Will find all elements of class that have no inverse refernces and will remove them, use very carefully."
|
||||
"Will find all elements of class that have no inverse refernces and will remove them, use very carefully"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
@@ -20,11 +20,12 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.SelectDiffJsonFile,
|
||||
operator.VisualiseDiff,
|
||||
operator.SelectDiffOldFile,
|
||||
operator.SelectDiffNewFile,
|
||||
operator.ExecuteIfcDiff,
|
||||
operator.SelectDiffJsonFile,
|
||||
operator.SelectDiffNewFile,
|
||||
operator.SelectDiffObjects,
|
||||
operator.SelectDiffOldFile,
|
||||
operator.VisualiseDiff,
|
||||
prop.Relationships,
|
||||
prop.DiffProperties,
|
||||
ui.BIM_PT_diff,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import ifccsv
|
||||
import logging
|
||||
import ifcopenshell
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
@@ -53,6 +53,16 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
for obj in context.visible_objects:
|
||||
obj.color = (1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
if "IfcDiff Deleted Elements" in obj.users_collection[0].name:
|
||||
obj.color = (1.0, 0.0, 0.0, 1.0)
|
||||
continue
|
||||
elif "IfcDiff Added Elements" in obj.users_collection[0].name:
|
||||
obj.color = (0.0, 1.0, 0.0, 1.0)
|
||||
continue
|
||||
elif "IfcDiff Changed Elements" in obj.users_collection[0].name:
|
||||
obj.color = (0.0, 0.0, 0.7, 1.0)
|
||||
continue
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
|
||||
@@ -135,14 +145,140 @@ class ExecuteIfcDiff(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
import ifcdiff
|
||||
|
||||
old = ifcopenshell.open(context.scene.DiffProperties.old_file)
|
||||
new = ifcopenshell.open(context.scene.DiffProperties.new_file)
|
||||
relationships = [r.relationship for r in context.scene.DiffProperties.diff_relationships]
|
||||
query = context.scene.DiffProperties.diff_filter_elements
|
||||
self.props = context.scene.DiffProperties
|
||||
|
||||
if tool.Ifc.get():
|
||||
if self.props.active_file == "NONE":
|
||||
old = ifcopenshell.open(self.props.old_file)
|
||||
new = ifcopenshell.open(self.props.new_file)
|
||||
elif self.props.active_file == "NEW":
|
||||
old = ifcopenshell.open(self.props.old_file)
|
||||
new = tool.Ifc.get()
|
||||
elif self.props.active_file == "OLD":
|
||||
old = tool.Ifc.get()
|
||||
new = ifcopenshell.open(self.props.new_file)
|
||||
else:
|
||||
old = ifcopenshell.open(self.props.old_file)
|
||||
new = ifcopenshell.open(self.props.new_file)
|
||||
|
||||
relationships = [r.relationship for r in self.props.diff_relationships]
|
||||
query = self.props.diff_filter_elements
|
||||
|
||||
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query)
|
||||
ifc_diff.diff()
|
||||
ifc_diff.export(self.filepath)
|
||||
context.scene.DiffProperties.diff_json_file = self.filepath
|
||||
self.props.diff_json_file = self.filepath
|
||||
|
||||
self.load_changed_elements(ifc_diff)
|
||||
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
def load_changed_elements(self, ifc_diff):
|
||||
if not tool.Ifc.get() or self.props.active_file == "NONE" or not self.props.should_load_changed_elements:
|
||||
return
|
||||
|
||||
active_ifc = tool.Ifc.get()
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
ifc_import_settings = blenderbim.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
|
||||
ifc_importer = blenderbim.bim.import_ifc.IfcImporter(ifc_import_settings)
|
||||
|
||||
if self.props.active_file == "NEW":
|
||||
tool.Ifc.set(ifc_diff.old)
|
||||
ifc_importer.file = ifc_diff.old
|
||||
ifc_importer.process_context_filter()
|
||||
ifc_importer.create_materials()
|
||||
ifc_importer.create_styles()
|
||||
|
||||
elements = {ifc_diff.old.by_guid(guid) for guid in ifc_diff.deleted_elements}
|
||||
ifc_importer.create_generic_elements(elements)
|
||||
self.place_objs_in_collection(ifc_importer.added_data.values(), "IfcDiff Deleted Elements")
|
||||
|
||||
ifc_importer.added_data = {}
|
||||
|
||||
elements = {ifc_diff.old.by_guid(guid) for guid in ifc_diff.change_register.keys()}
|
||||
ifc_importer.create_generic_elements(elements)
|
||||
self.place_objs_in_collection(ifc_importer.added_data.values(), "IfcDiff Changed Elements")
|
||||
elif self.props.active_file == "OLD":
|
||||
tool.Ifc.set(ifc_diff.new)
|
||||
ifc_importer.file = ifc_diff.new
|
||||
ifc_importer.process_context_filter()
|
||||
ifc_importer.create_materials()
|
||||
ifc_importer.create_styles()
|
||||
|
||||
elements = {ifc_diff.new.by_guid(guid) for guid in ifc_diff.added_elements}
|
||||
ifc_importer.create_generic_elements(elements)
|
||||
self.place_objs_in_collection(ifc_importer.added_data.values(), "IfcDiff Added Elements")
|
||||
|
||||
ifc_importer.added_data = {}
|
||||
|
||||
elements = {ifc_diff.new.by_guid(guid) for guid in ifc_diff.change_register.keys()}
|
||||
ifc_importer.create_generic_elements(elements)
|
||||
self.place_objs_in_collection(ifc_importer.added_data.values(), "IfcDiff Changed Elements")
|
||||
tool.Ifc.set(active_ifc)
|
||||
|
||||
def place_objs_in_collection(self, objs, name):
|
||||
collection = bpy.data.collections.get(name)
|
||||
if not collection:
|
||||
collection = bpy.data.collections.new(name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
|
||||
for obj in objs:
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
collection.objects.link(obj)
|
||||
|
||||
|
||||
class SelectDiffObjects(bpy.types.Operator):
|
||||
bl_idname = "bim.select_diff_objects"
|
||||
bl_label = "Select Diff Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
mode: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
|
||||
diff = json.load(file)
|
||||
for obj in context.visible_objects:
|
||||
obj.select_set(False)
|
||||
|
||||
if self.mode == "DELETED" and "IfcDiff Deleted Elements" in obj.users_collection[0].name:
|
||||
obj.select_set(True)
|
||||
continue
|
||||
elif self.mode == "ADDED" and "IfcDiff Added Elements" in obj.users_collection[0].name:
|
||||
obj.select_set(True)
|
||||
continue
|
||||
elif self.mode == "CHANGED" and "IfcDiff Changed Elements" in obj.users_collection[0].name:
|
||||
obj.select_set(True)
|
||||
continue
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
for scene in obj.users_scene:
|
||||
if scene.BIMProperties.ifc_file:
|
||||
ifc_file = scene.BIMProperties.ifc_file
|
||||
if scene.library:
|
||||
break
|
||||
|
||||
if ifc_file:
|
||||
if ifc_file not in IfcStore.session_files:
|
||||
IfcStore.session_files[ifc_file] = ifcopenshell.open(ifc_file)
|
||||
element_file = IfcStore.session_files[ifc_file]
|
||||
else:
|
||||
element_file = ifc_file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
except:
|
||||
continue
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
if not global_id:
|
||||
continue
|
||||
if self.mode == "DELETED" and global_id in diff["deleted"]:
|
||||
obj.select_set(True)
|
||||
elif self.mode == "ADDED" and global_id in diff["added"]:
|
||||
obj.select_set(True)
|
||||
elif self.mode == "CHANGED" and global_id in diff["changed"]:
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -49,3 +49,13 @@ class DiffProperties(PropertyGroup):
|
||||
new_file: StringProperty(default="", name="New IFC File")
|
||||
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
|
||||
diff_filter_elements: StringProperty(default="", name="Filter")
|
||||
should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True)
|
||||
active_file: EnumProperty(
|
||||
items=[
|
||||
("NONE", "N/A", ""),
|
||||
("OLD", "Old", ""),
|
||||
("NEW", "New", ""),
|
||||
],
|
||||
name="Active File",
|
||||
default="NEW",
|
||||
)
|
||||
|
||||
@@ -44,13 +44,38 @@ class BIM_PT_diff(Panel):
|
||||
|
||||
layout.label(text="IFC Diff Setup:")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "old_file")
|
||||
row.operator("bim.select_diff_old_file", icon="FILE_FOLDER", text="")
|
||||
if tool.Ifc.get():
|
||||
row = layout.row()
|
||||
row.prop(props, "active_file")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "new_file")
|
||||
row.operator("bim.select_diff_new_file", icon="FILE_FOLDER", text="")
|
||||
if props.active_file == "NONE":
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "old_file")
|
||||
row.operator("bim.select_diff_old_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "new_file")
|
||||
row.operator("bim.select_diff_new_file", icon="FILE_FOLDER", text="")
|
||||
elif props.active_file == "NEW":
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "old_file")
|
||||
row.operator("bim.select_diff_old_file", icon="FILE_FOLDER", text="")
|
||||
elif props.active_file == "OLD":
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "new_file")
|
||||
row.operator("bim.select_diff_new_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
if props.active_file != "NONE":
|
||||
row = layout.row()
|
||||
row.prop(props, "should_load_changed_elements")
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "old_file")
|
||||
row.operator("bim.select_diff_old_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "new_file")
|
||||
row.operator("bim.select_diff_new_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "diff_relationships")
|
||||
@@ -82,10 +107,22 @@ class BIM_PT_diff(Panel):
|
||||
|
||||
if DiffData.data["diff_json"]:
|
||||
row = layout.row()
|
||||
row.alignment = "CENTER"
|
||||
row.label(text=f"{DiffData.data['total_added']} added")
|
||||
row.label(text=f"{DiffData.data['total_deleted']} deleted")
|
||||
row.label(text=f"{DiffData.data['total_changed']} changed")
|
||||
col = row.column(align=True)
|
||||
col.operator(
|
||||
"bim.select_diff_objects",
|
||||
icon="RESTRICT_SELECT_OFF",
|
||||
text=f"Select {DiffData.data['total_added']} Added",
|
||||
).mode = "ADDED"
|
||||
col.operator(
|
||||
"bim.select_diff_objects",
|
||||
icon="RESTRICT_SELECT_OFF",
|
||||
text=f"Select {DiffData.data['total_deleted']} Deleted",
|
||||
).mode = "DELETED"
|
||||
col.operator(
|
||||
"bim.select_diff_objects",
|
||||
icon="RESTRICT_SELECT_OFF",
|
||||
text=f"Select {DiffData.data['total_changed']} Changed",
|
||||
).mode = "CHANGED"
|
||||
|
||||
if DiffData.data["changes"]:
|
||||
box = layout.box()
|
||||
|
||||
@@ -30,11 +30,13 @@ classes = (
|
||||
operator.AddDrawingStyleAttribute,
|
||||
operator.AddDrawingToSheet,
|
||||
operator.AddReference,
|
||||
operator.AddReferenceImage,
|
||||
operator.AddReferenceToSheet,
|
||||
operator.AddSchedule,
|
||||
operator.AddScheduleToSheet,
|
||||
operator.AddSheet,
|
||||
operator.AddTextLiteral,
|
||||
operator.AssignSelectedObjectAsProduct,
|
||||
operator.BuildSchedule,
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
@@ -50,11 +52,13 @@ classes = (
|
||||
operator.DisableEditingText,
|
||||
operator.DuplicateDrawing,
|
||||
operator.EditAssignedProduct,
|
||||
operator.EditElementFilter,
|
||||
operator.EditSheet,
|
||||
operator.EditText,
|
||||
operator.EditTextPopup,
|
||||
operator.EnableAddAnnotationType,
|
||||
operator.EnableEditingAssignedProduct,
|
||||
operator.EnableEditingElementFilter,
|
||||
operator.EnableEditingText,
|
||||
operator.ExpandSheet,
|
||||
operator.ExpandTargetView,
|
||||
@@ -95,6 +99,7 @@ classes = (
|
||||
ui.BIM_PT_sheets,
|
||||
ui.BIM_PT_drawings,
|
||||
ui.BIM_PT_camera,
|
||||
ui.BIM_PT_element_filters,
|
||||
ui.BIM_PT_drawing_underlay,
|
||||
ui.BIM_PT_schedules,
|
||||
ui.BIM_PT_references,
|
||||
@@ -123,6 +128,7 @@ def register():
|
||||
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
|
||||
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
||||
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
|
||||
|
||||
|
||||
def unregister():
|
||||
@@ -136,3 +142,4 @@ def unregister():
|
||||
del bpy.types.TextCurve.BIMTextProperties
|
||||
bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad)
|
||||
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
|
||||
bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button)
|
||||
|
||||
@@ -29,6 +29,7 @@ def refresh():
|
||||
SheetsData.is_loaded = False
|
||||
DocumentsData.is_loaded = False
|
||||
DrawingsData.is_loaded = False
|
||||
ElementFiltersData.is_loaded = False
|
||||
AnnotationData.is_loaded = False
|
||||
DecoratorData.data = {}
|
||||
DecoratorData.cut_cache = {}
|
||||
@@ -137,6 +138,55 @@ class DrawingsData:
|
||||
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
||||
|
||||
|
||||
class ElementFiltersData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"saved_searches": cls.saved_searches(),
|
||||
"has_include_filter": cls.has_include_filter(),
|
||||
"has_exclude_filter": cls.has_exclude_filter(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@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")]
|
||||
|
||||
@classmethod
|
||||
def has_include_filter(cls):
|
||||
obj = bpy.context.scene.camera
|
||||
if not obj:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
return bool(ifcopenshell.util.element.get_pset(element, "EPset_Drawing", "Include"))
|
||||
|
||||
@classmethod
|
||||
def has_exclude_filter(cls):
|
||||
obj = bpy.context.scene.camera
|
||||
if not obj:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
return bool(ifcopenshell.util.element.get_pset(element, "EPset_Drawing", "Exclude"))
|
||||
|
||||
|
||||
class DocumentsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@@ -161,9 +161,9 @@ class BaseDecorator:
|
||||
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
|
||||
)
|
||||
|
||||
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.base_shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.base_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
def get_camera_width_mm(self):
|
||||
# Horrific prototype code to ensure bgl draws at drawing scales
|
||||
@@ -390,8 +390,6 @@ class BaseDecorator:
|
||||
# 0 is the default font, but we're fancier than that
|
||||
font_id = self.font_id
|
||||
|
||||
dpi = context.preferences.system.dpi
|
||||
|
||||
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
|
||||
|
||||
ang = -Vector((1, 0)).angle_signed(text_dir)
|
||||
@@ -411,7 +409,7 @@ class BaseDecorator:
|
||||
font_size_px = int(0.004118616 * mm_to_px) * font_size_mm / 2.5
|
||||
pos = pos - line_no * font_size_px * rotation_matrix[1]
|
||||
|
||||
blf.size(font_id, font_size_px, dpi)
|
||||
blf.size(font_id, font_size_px)
|
||||
|
||||
if box_alignment or center or vcenter:
|
||||
w, h = blf.dimensions(font_id, text)
|
||||
@@ -803,7 +801,7 @@ class AngleDecorator(BaseDecorator):
|
||||
angle_rad = acos(cos_a)
|
||||
angle = angle_rad / pi * 180
|
||||
except ZeroDivisionError:
|
||||
angle = 0
|
||||
angle_rad, angle = 0, 0
|
||||
|
||||
# calculate angle position
|
||||
p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]]
|
||||
@@ -1563,15 +1561,15 @@ class CutDecorator:
|
||||
gpu.state.point_size_set(1)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
|
||||
black = (0, 0, 0, 1)
|
||||
@@ -1931,7 +1929,7 @@ class DecorationsHandler:
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
# NOTE: we USE POST_PIXEL here so that we can use both 3D_POLYLINE_UNIFORM_COLOR
|
||||
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
|
||||
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
||||
|
||||
|
||||
@@ -41,15 +41,16 @@ import blenderbim.bim.module.drawing.annotation as annotation
|
||||
import blenderbim.bim.module.drawing.sheeter as sheeter
|
||||
import blenderbim.bim.module.drawing.scheduler as scheduler
|
||||
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
|
||||
import blenderbim.bim.export_ifc
|
||||
from lxml import etree
|
||||
from mathutils import Vector, Color
|
||||
from mathutils import Vector, Color, Matrix
|
||||
from timeit import default_timer as timer
|
||||
from blenderbim.bim.module.drawing.prop import RasterStyleProperty, Literal, RASTER_STYLE_PROPERTIES_EXCLUDE
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from pathlib import Path
|
||||
from bpy_extras.image_utils import load_image
|
||||
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
@@ -103,6 +104,9 @@ class AddAnnotationType(bpy.types.Operator, Operator):
|
||||
)
|
||||
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
|
||||
|
||||
if props.create_representation_for_type and object_type == "IMAGE":
|
||||
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
|
||||
|
||||
|
||||
class EnableAddAnnotationType(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.enable_add_annotation_type"
|
||||
@@ -259,7 +263,9 @@ class CreateDrawing(bpy.types.Operator):
|
||||
with profile("Combine SVG layers"):
|
||||
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
|
||||
|
||||
tool.Drawing.open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
|
||||
tool.Drawing.open_with_user_command(
|
||||
context.preferences.addons["blenderbim"].preferences.svg_command, svg_path
|
||||
)
|
||||
|
||||
if self.print_all:
|
||||
bpy.ops.bim.activate_drawing(drawing=original_drawing_id, camera_view_point=False)
|
||||
@@ -534,7 +540,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
root = etree.fromstring(results)
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
if not group:
|
||||
if group is None:
|
||||
with open(svg_path, "wb") as svg:
|
||||
svg.write(etree.tostring(root))
|
||||
|
||||
@@ -1420,25 +1426,8 @@ class ActivateModel(bpy.types.Operator):
|
||||
CutDecorator.uninstall()
|
||||
|
||||
if not bpy.app.background:
|
||||
view3d_context = dict()
|
||||
|
||||
for window in bpy.context.window_manager.windows:
|
||||
screen = window.screen
|
||||
for area in screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
view3d_context["window"] = window
|
||||
view3d_context["screen"] = screen
|
||||
view3d_context["area"] = area
|
||||
for region in area.regions:
|
||||
if region.type == "WINDOW":
|
||||
view3d_context["region"] = region
|
||||
break
|
||||
break
|
||||
if view3d_context:
|
||||
break
|
||||
|
||||
if view3d_context:
|
||||
bpy.ops.object.hide_view_clear(view3d_context)
|
||||
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")
|
||||
@@ -2282,6 +2271,27 @@ class RemoveTextLiteral(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AssignSelectedObjectAsProduct(bpy.types.Operator):
|
||||
bl_idname = "bim.assign_selected_as_product"
|
||||
bl_label = "Assign Selected Object As Product"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if len(context.selected_objects) != 2:
|
||||
cls.poll_message_set("2 objects need to be selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
objs = context.selected_objects[:]
|
||||
objs.remove(context.active_object)
|
||||
other_selected_object = next(obj for obj in context.selected_objects if obj != context.active_object)
|
||||
context.active_object.BIMAssignedProductProperties.relating_product = other_selected_object
|
||||
bpy.ops.bim.edit_assigned_product()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditAssignedProduct(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.edit_assigned_product"
|
||||
bl_label = "Edit Text Product"
|
||||
@@ -2521,3 +2531,156 @@ class SelectAssignedProduct(bpy.types.Operator, Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
core.select_assigned_product(tool.Drawing, context)
|
||||
|
||||
|
||||
class EnableEditingElementFilter(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.enable_editing_element_filter"
|
||||
bl_label = "Enable Editing Element Filter"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_mode: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.context.scene.camera
|
||||
if obj:
|
||||
obj.data.BIMCameraProperties.filter_mode = self.filter_mode
|
||||
|
||||
|
||||
class EditElementFilter(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.edit_element_filter"
|
||||
bl_label = "Edit Element Filter"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_mode: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.active_object.data.BIMCameraProperties
|
||||
obj = bpy.context.scene.camera
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
|
||||
if self.filter_mode == "INCLUDE":
|
||||
query = tool.Search.export_filter_query(props.include_filter_groups) or None
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Include": query})
|
||||
elif self.filter_mode == "EXCLUDE":
|
||||
query = tool.Search.export_filter_query(props.exclude_filter_groups) or None
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Exclude": query})
|
||||
obj.data.BIMCameraProperties.filter_mode = "NONE"
|
||||
bpy.ops.bim.activate_drawing(drawing=element.id(), camera_view_point=False)
|
||||
|
||||
|
||||
class AddReferenceImage(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.add_reference_image"
|
||||
bl_label = "Add Reference Image"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||
filepath: bpy.props.StringProperty(
|
||||
name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH"
|
||||
)
|
||||
filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
|
||||
filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
override_existing_image: bpy.props.BoolProperty(
|
||||
name="Override Existing Image",
|
||||
default=True,
|
||||
description=(
|
||||
"Override image if it was previously loaded to Blender. If disabled, will always create a new image"
|
||||
),
|
||||
)
|
||||
use_existing_object_by_name: bpy.props.StringProperty(
|
||||
name="Use Existing Object By Name",
|
||||
description="Existing object name to add a style with reference image to. If not provided will create a new object.",
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if Path(tool.Ifc.get_path()).is_file():
|
||||
layout.prop(self, "use_relative_path")
|
||||
else:
|
||||
self.use_relative_path = False
|
||||
layout.label(text="Save the .ifc file first ")
|
||||
layout.label(text="to use relative paths.")
|
||||
layout.prop(self, "override_existing_image")
|
||||
layout.prop(self, "use_existing_object_by_name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _execute(self, context):
|
||||
abs_path = Path(self.filepath)
|
||||
if self.use_relative_path:
|
||||
image_filepath = abs_path.relative_to(Path(tool.Ifc.get_path()).parent)
|
||||
else:
|
||||
image_filepath = abs_path
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
if self.override_existing_image:
|
||||
params = {"check_existing": True, "force_reload": True}
|
||||
else:
|
||||
params = {"check_existing": False}
|
||||
image = load_image(abs_path.name, abs_path.parent, **params)
|
||||
|
||||
def bm_add_image_plane(mesh):
|
||||
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True)
|
||||
plane_scale = (Vector(image.size) / min(image.size)).to_3d()
|
||||
matrix = Matrix.LocRotScale(None, None, plane_scale)
|
||||
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False)
|
||||
tool.Blender.apply_bmesh(mesh, bm)
|
||||
|
||||
if self.use_existing_object_by_name:
|
||||
obj = bpy.data.objects[self.use_existing_object_by_name]
|
||||
bm_add_image_plane(obj.data)
|
||||
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
|
||||
else:
|
||||
temp_mesh = bpy.data.meshes.new("temp_mesh")
|
||||
bm_add_image_plane(temp_mesh)
|
||||
obj = bpy.data.objects.new(image_filepath.stem, temp_mesh)
|
||||
tool.Drawing.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcAnnotation",
|
||||
predefined_type="IMAGE",
|
||||
should_add_representation=True,
|
||||
context=ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW"),
|
||||
ifc_representation_class=None,
|
||||
)
|
||||
tool.Blender.remove_data_block(temp_mesh)
|
||||
|
||||
tool.Blender.set_active_object(obj)
|
||||
|
||||
material = bpy.data.materials.new(name=image_filepath.stem)
|
||||
obj.data.materials.append(None) # new slot
|
||||
obj.material_slots[0].material = material
|
||||
bpy.ops.bim.add_style()
|
||||
|
||||
style = ifc_file.by_id(material.BIMMaterialProperties.ifc_style_id)
|
||||
tool.Style.assign_style_to_object(style, obj)
|
||||
|
||||
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
|
||||
# we don't support IfcSurfaceStyleWithTextures without Rendering yet
|
||||
shading_attributes = {
|
||||
"SurfaceColour": {
|
||||
"Red": 1.0,
|
||||
"Green": 1.0,
|
||||
"Blue": 1.0,
|
||||
},
|
||||
"Transparency": 0.0,
|
||||
"ReflectanceMethod": "NOTDEFINED",
|
||||
}
|
||||
ifcopenshell.api.run(
|
||||
"style.add_surface_style",
|
||||
tool.Ifc.get(),
|
||||
style=style,
|
||||
ifc_class="IfcSurfaceStyleRendering",
|
||||
attributes=shading_attributes,
|
||||
)
|
||||
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
|
||||
textures = [texture]
|
||||
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map
|
||||
tool.Ifc.run(
|
||||
"style.add_surface_style",
|
||||
style=style,
|
||||
ifc_class="IfcSurfaceStyleWithTextures",
|
||||
attributes={"Textures": textures},
|
||||
)
|
||||
tool.Style.reload_material_from_ifc(material)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
@@ -25,6 +25,7 @@ import blenderbim.tool as tool
|
||||
import blenderbim.core.drawing as core
|
||||
import blenderbim.bim.module.drawing.annotation as annotation
|
||||
import blenderbim.bim.module.drawing.decoration as decoration
|
||||
from blenderbim.bim.prop import BIMFilterGroup
|
||||
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
|
||||
from blenderbim.bim.module.drawing.data import refresh as refresh_drawing_data
|
||||
from pathlib import Path
|
||||
@@ -379,6 +380,9 @@ class BIMCameraProperties(PropertyGroup):
|
||||
height: FloatProperty(name="Height", default=50, subtype="DISTANCE")
|
||||
is_nts: BoolProperty(name="Is NTS", update=update_is_nts)
|
||||
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
|
||||
filter_mode: StringProperty(name="Filter Mode", default="NONE")
|
||||
include_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Include Filter")
|
||||
exclude_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Exclude Filter")
|
||||
|
||||
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
|
||||
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
|
||||
@@ -511,6 +515,7 @@ ANNOTATION_TYPES_DATA = {
|
||||
"REVISION_CLOUD":("Revision Cloud", "Add revision cloud", "VOLUME_DATA", "mesh"),
|
||||
"FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"),
|
||||
"FALL": ("Fall", "", "SORT_ASC", "curve"),
|
||||
"IMAGE": ("Image", "Add reference image attached to the drawing", "TEXTURE", "mesh"),
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -288,9 +288,9 @@ class BaseShader:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.base_shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.base_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
def get_shader(self):
|
||||
"""Returns shader for this type"""
|
||||
|
||||
@@ -160,7 +160,7 @@ class SvgWriter:
|
||||
def find_xml_symbol_by_id(self, id):
|
||||
tree = ET.parse(self.resource_paths["Symbols"])
|
||||
xml_symbol = tree.find(f'.//*[@id="{id}"]')
|
||||
return External(xml_symbol) if xml_symbol else None
|
||||
return External(xml_symbol) if xml_symbol is not None else None
|
||||
|
||||
def add_patterns(self):
|
||||
path = self.resource_paths["Patterns"]
|
||||
|
||||
@@ -25,6 +25,7 @@ from blenderbim.bim.module.drawing.data import (
|
||||
SheetsData,
|
||||
DocumentsData,
|
||||
DrawingsData,
|
||||
ElementFiltersData,
|
||||
DecoratorData,
|
||||
)
|
||||
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
|
||||
@@ -95,6 +96,60 @@ class BIM_PT_camera(Panel):
|
||||
op.view = context.scene.camera.name.split("/")[1]
|
||||
|
||||
|
||||
class BIM_PT_element_filters(Panel):
|
||||
bl_label = "Element Filters"
|
||||
bl_idname = "BIM_PT_element_filters"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_camera"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.scene.camera
|
||||
and context.active_object
|
||||
and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
if not ElementFiltersData.is_loaded:
|
||||
ElementFiltersData.load()
|
||||
|
||||
props = context.scene.camera.data.BIMCameraProperties
|
||||
|
||||
if props.filter_mode == "INCLUDE":
|
||||
blenderbim.bim.helper.draw_filter(
|
||||
self.layout, props.include_filter_groups, ElementFiltersData, "drawing_include"
|
||||
)
|
||||
row = self.layout.row(align=True)
|
||||
row.operator(
|
||||
"bim.edit_element_filter", icon="CHECKMARK", text="Save Include Filter"
|
||||
).filter_mode = "INCLUDE"
|
||||
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
|
||||
elif props.filter_mode == "EXCLUDE":
|
||||
blenderbim.bim.helper.draw_filter(
|
||||
self.layout, props.exclude_filter_groups, ElementFiltersData, "drawing_exclude"
|
||||
)
|
||||
row = self.layout.row(align=True)
|
||||
row.operator(
|
||||
"bim.edit_element_filter", icon="CHECKMARK", text="Save Exclude Filter"
|
||||
).filter_mode = "EXCLUDE"
|
||||
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
text = "Include Filter" if ElementFiltersData.data["has_include_filter"] else "No Include Filter Found"
|
||||
icon = "GREASEPENCIL" if ElementFiltersData.data["has_include_filter"] else "ADD"
|
||||
row.label(text=text, icon="FILTER")
|
||||
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "INCLUDE"
|
||||
row = self.layout.row(align=True)
|
||||
text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
|
||||
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
|
||||
row.label(text=text, icon="FILTER")
|
||||
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
|
||||
|
||||
|
||||
class BIM_PT_drawing_underlay(Panel):
|
||||
bl_label = "Drawing Underlay"
|
||||
bl_idname = "BIM_PT_drawing_underlay"
|
||||
@@ -106,7 +161,11 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.camera and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
return (
|
||||
context.scene.camera
|
||||
and context.active_object
|
||||
and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -409,13 +468,16 @@ class BIM_PT_product_assignments(Panel):
|
||||
if props.is_editing_product:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "relating_product", text="")
|
||||
row.operator("bim.assign_selected_as_product", icon="OBJECT_DATA", text="")
|
||||
row.operator("bim.edit_assigned_product", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_assigned_product", icon="CANCEL", text="")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=ProductAssignmentsData.data["relating_product"] or "No Relating Product", icon="OBJECT_DATA")
|
||||
row.operator("bim.enable_editing_assigned_product", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.select_assigned_product", icon="RESTRICT_SELECT_OFF", text="")
|
||||
col = row.column()
|
||||
col.operator("bim.select_assigned_product", icon="RESTRICT_SELECT_OFF", text="")
|
||||
col.enabled = bool(ProductAssignmentsData.data["relating_product"])
|
||||
|
||||
|
||||
class BIM_PT_text(Panel):
|
||||
@@ -597,3 +659,7 @@ class BIM_UL_sheets(bpy.types.UIList):
|
||||
if not flt_flags:
|
||||
return flt_flags, flt_neworder
|
||||
return flt_flags, flt_neworder
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator("bim.add_reference_image", icon="TEXTURE", text="IFC Reference Image")
|
||||
|
||||
@@ -186,6 +186,9 @@ def create_annotation_occurrence(context):
|
||||
tool.Collector.assign(obj)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
|
||||
if relating_type_repr is None and props.object_type == "IMAGE":
|
||||
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
|
||||
|
||||
|
||||
def create_annotation():
|
||||
props = bpy.context.scene.BIMAnnotationProperties
|
||||
@@ -194,6 +197,10 @@ def create_annotation():
|
||||
else:
|
||||
object_type = props.object_type
|
||||
bpy.ops.bim.add_annotation(object_type=object_type, data_type=ANNOTATION_TYPES_DATA[object_type][-1])
|
||||
if props.object_type == "IMAGE":
|
||||
bpy.ops.bim.add_reference_image(
|
||||
"INVOKE_DEFAULT", use_existing_object_by_name=bpy.context.active_object.name
|
||||
)
|
||||
|
||||
|
||||
class AnnotationToolUI:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import ifcfm
|
||||
import importlib
|
||||
|
||||
|
||||
def refresh():
|
||||
FMData.is_loaded = False
|
||||
|
||||
|
||||
class FMData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data = {}
|
||||
cls.data["engine"] = cls.engine()
|
||||
|
||||
@classmethod
|
||||
def engine(cls):
|
||||
results = []
|
||||
fm_dir = os.path.dirname(ifcfm.__file__)
|
||||
for f in os.listdir(fm_dir):
|
||||
if f.endswith(".py") and not f.startswith("_"):
|
||||
preset = os.path.splitext(f)[0]
|
||||
module = importlib.import_module(f"ifcfm.{preset}")
|
||||
config = getattr(module, "config")
|
||||
results.append((preset, config["name"], config["description"]))
|
||||
return results
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.module.fm.data import FMData
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -31,21 +32,18 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def get_engine(self, context):
|
||||
if not FMData.is_loaded:
|
||||
FMData.load()
|
||||
return FMData.data["engine"]
|
||||
|
||||
|
||||
class BIMFMProperties(PropertyGroup):
|
||||
ifc_file: StringProperty(default="", name="IFC File")
|
||||
ifc_files: CollectionProperty(name="IFC Files", type=StrProperty)
|
||||
spreadsheet_files: CollectionProperty(name="Spreadsheets", type=StrProperty)
|
||||
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
|
||||
engine: EnumProperty(
|
||||
items=[
|
||||
("aohbsem", "AOH-BSEM", ""),
|
||||
("basic", "Basic", ""),
|
||||
("cobie24", "COBie 2.4", ""),
|
||||
("cobie3", "COBie 3", ""),
|
||||
],
|
||||
name="Engine",
|
||||
default="cobie24",
|
||||
)
|
||||
engine: EnumProperty(items=get_engine, name="Engine")
|
||||
format: EnumProperty(
|
||||
items=[
|
||||
("csv", "csv", ""),
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.fm.data import FMData
|
||||
|
||||
|
||||
class BIM_PT_fm(Panel):
|
||||
@@ -30,6 +31,9 @@ class BIM_PT_fm(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_handover"
|
||||
|
||||
def draw(self, context):
|
||||
if not FMData.is_loaded:
|
||||
FMData.load()
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ classes = (
|
||||
operator.EnableEditingRepresentationItems,
|
||||
operator.FlipObject,
|
||||
operator.GetRepresentationIfcParameters,
|
||||
operator.DuplicateMoveLinkedAggregate,
|
||||
operator.DuplicateMoveLinkedAggregateMacro,
|
||||
operator.OverrideDelete,
|
||||
operator.OverrideDuplicateMove,
|
||||
operator.OverrideDuplicateMoveAggregate,
|
||||
operator.OverrideDuplicateMoveAggregateMacro,
|
||||
operator.OverrideDuplicateMoveLinked,
|
||||
operator.OverrideDuplicateMoveLinkedMacro,
|
||||
operator.OverrideDuplicateMoveMacro,
|
||||
@@ -42,7 +42,7 @@ classes = (
|
||||
operator.OverrideOutlinerDelete,
|
||||
operator.OverridePasteBuffer,
|
||||
operator.PurgeUnusedRepresentations,
|
||||
operator.RefreshAggregate,
|
||||
operator.RefreshLinkedAggregate,
|
||||
operator.RemoveConnection,
|
||||
operator.RemoveRepresentation,
|
||||
operator.SelectConnection,
|
||||
@@ -61,6 +61,7 @@ classes = (
|
||||
ui.BIM_PT_workarounds,
|
||||
ui.BIM_MT_object_set_origin,
|
||||
ui.BIM_MT_separate,
|
||||
ui.BIM_MT_hotkey_separate,
|
||||
ui.BIM_UL_representation_items,
|
||||
)
|
||||
|
||||
@@ -73,8 +74,8 @@ def register():
|
||||
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
|
||||
operator.OverrideDuplicateMoveAggregateMacro.define("BIM_OT_override_object_duplicate_move_aggregate")
|
||||
operator.OverrideDuplicateMoveAggregateMacro.define("TRANSFORM_OT_translate")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
|
||||
|
||||
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
|
||||
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
|
||||
@@ -91,7 +92,7 @@ def register():
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_aggregate_macro", "D", "PRESS", ctrl=True, shift=True)
|
||||
kmi = km.keymap_items.new("bim.object_duplicate_move_linked_aggregate_macro", "D", "PRESS", ctrl=True, shift=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
@@ -106,6 +107,9 @@ def register():
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
|
||||
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Curve", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
|
||||
@@ -101,8 +101,6 @@ class RepresentationItemsData:
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"total_items": cls.total_items(),
|
||||
"active_surface_style": cls.active_surface_style(),
|
||||
"active_layer": cls.active_layer(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@@ -124,18 +122,6 @@ class RepresentationItemsData:
|
||||
result += 1
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def active_surface_style(cls):
|
||||
props = bpy.context.active_object.BIMGeometryProperties
|
||||
if props.active_item_index < len(props.items):
|
||||
return props.items[props.active_item_index].surface_style
|
||||
|
||||
@classmethod
|
||||
def active_layer(cls):
|
||||
props = bpy.context.active_object.BIMGeometryProperties
|
||||
if props.active_item_index < len(props.items):
|
||||
return props.items[props.active_item_index].layer
|
||||
|
||||
|
||||
class ConnectionsData:
|
||||
data = {}
|
||||
@@ -322,6 +308,9 @@ class PlacementData:
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"has_placement": cls.has_placement(),
|
||||
"original_x": cls.original_x(),
|
||||
"original_y": cls.original_y(),
|
||||
"original_z": cls.original_z(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@@ -331,3 +320,42 @@ class PlacementData:
|
||||
if element and hasattr(element, "ObjectPlacement"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def original_x(cls):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
obj = bpy.context.active_object
|
||||
if not obj or not props.has_blender_offset:
|
||||
return
|
||||
return str(round(cls.original_xyz(obj.location)[0], 3))
|
||||
|
||||
@classmethod
|
||||
def original_y(cls):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
obj = bpy.context.active_object
|
||||
if not obj or not props.has_blender_offset:
|
||||
return
|
||||
return str(round(cls.original_xyz(obj.location)[1], 3))
|
||||
|
||||
@classmethod
|
||||
def original_z(cls):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
obj = bpy.context.active_object
|
||||
if not obj or not props.has_blender_offset:
|
||||
return
|
||||
return str(round(cls.original_xyz(obj.location)[2], 3))
|
||||
|
||||
@classmethod
|
||||
def original_xyz(cls, location):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
return ifcopenshell.util.geolocation.xyz2enh(
|
||||
location[0],
|
||||
location[1],
|
||||
location[2],
|
||||
float(props.blender_eastings),
|
||||
float(props.blender_northings),
|
||||
float(props.blender_orthogonal_height),
|
||||
float(props.blender_x_axis_abscissa),
|
||||
float(props.blender_x_axis_ordinate),
|
||||
1.0,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,6 @@ import blenderbim.tool as tool
|
||||
import blenderbim.bim.handler
|
||||
from mathutils import Vector, Matrix
|
||||
from time import time
|
||||
from blenderbim.bim import import_ifc
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
@@ -67,9 +66,12 @@ class OverrideMeshSeparate(bpy.types.Operator, Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
|
||||
# You cannot separate meshes if the representation is mapped.
|
||||
relating_type = tool.Root.get_element_type(tool.Ifc.get_entity(obj))
|
||||
relating_type = tool.Root.get_element_type(element)
|
||||
if relating_type and tool.Root.does_type_have_representations(relating_type):
|
||||
# We toggle edit mode to ensure that once representations are
|
||||
# unmapped, our Blender mesh only has a single user.
|
||||
@@ -273,7 +275,7 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
ifc_representation_class: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
if context.active_object and context.active_object.mode != "OBJECT":
|
||||
if context.view_layer.objects.active and context.view_layer.objects.active.mode != "OBJECT":
|
||||
# Ensure mode is object to prevent invalid mesh data causing CTD
|
||||
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
|
||||
|
||||
@@ -372,14 +374,14 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
tool.Geometry.run_style_add_style(obj=mat)
|
||||
for mat in tool.Geometry.get_object_materials_without_styles(obj)
|
||||
]
|
||||
ifcopenshell.api.run(
|
||||
"style.assign_representation_styles",
|
||||
self.file,
|
||||
shape_representation=new_representation,
|
||||
styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True),
|
||||
should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
ifcopenshell.api.run(
|
||||
"style.assign_representation_styles",
|
||||
self.file,
|
||||
shape_representation=new_representation,
|
||||
styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True),
|
||||
should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
# TODO: move this into a replace_representation usecase or something
|
||||
for inverse in self.file.get_inverse(old_representation):
|
||||
@@ -733,7 +735,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
new_active_obj = None
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if linked and obj.data:
|
||||
if obj.data and not linked:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
new_active_obj = new_obj
|
||||
@@ -757,7 +759,8 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
|
||||
self.new_active_obj = None
|
||||
# Track decompositions so they can be recreated after the operation
|
||||
relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate)
|
||||
decomposition_relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate)
|
||||
connection_relationships = tool.Root.get_connection_relationships(objects_to_duplicate)
|
||||
old_to_new = {}
|
||||
|
||||
for obj in objects_to_duplicate:
|
||||
@@ -797,7 +800,12 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
# clean up the orphaned mesh with ifc id of the original object to avoid confusion
|
||||
# IfcGridAxis keeps the same mesh data (it's pointing to ifc id 0, so it's not a problem)
|
||||
if new and temp_data and not new.is_a("IfcGridAxis"):
|
||||
tool.Blender.remove_data_block(temp_data)
|
||||
if new.is_a("IfcRelSpaceBoundary"):
|
||||
surface = new.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
temp_data.name = f"0/{surface.id()}"
|
||||
temp_data.BIMMeshProperties.ifc_definition_id = surface.id()
|
||||
else:
|
||||
tool.Blender.remove_data_block(temp_data)
|
||||
|
||||
if new:
|
||||
# TODO: handle array data for other cases of duplication
|
||||
@@ -812,9 +820,19 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
if new.is_a("IfcRelSpaceBoundary"):
|
||||
tool.Boundary.decorate_boundary(new_obj)
|
||||
|
||||
# Recreate aggregate relationship
|
||||
for old in old_to_new.keys():
|
||||
if old.is_a("IfcElementAssembly"):
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
# Remove connections with old objects and recreates paths
|
||||
OverrideDuplicateMove.remove_old_connections(old_to_new)
|
||||
tool.Root.recreate_connections(connection_relationships, old_to_new)
|
||||
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return old_to_new
|
||||
|
||||
@staticmethod
|
||||
def process_arrays(self, context):
|
||||
@@ -853,6 +871,18 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
return arrays_to_create, array_children
|
||||
|
||||
|
||||
def remove_old_connections(old_to_new):
|
||||
for new in old_to_new.values():
|
||||
for connection in new[0].ConnectedTo:
|
||||
entity = connection.RelatedElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
for connection in new[0].ConnectedFrom:
|
||||
entity = connection.RelatingElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
|
||||
|
||||
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
|
||||
bl_idname = "bim.override_object_duplicate_move_linked_macro"
|
||||
bl_label = "IFC Duplicate Linked"
|
||||
@@ -875,15 +905,15 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
|
||||
class OverrideDuplicateMoveAggregateMacro(bpy.types.Macro):
|
||||
bl_idname = "bim.override_object_duplicate_move_aggregate_macro"
|
||||
bl_label = "IFC Duplicate Objects Aggregate"
|
||||
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
|
||||
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
|
||||
bl_label = "IFC Duplicate Linked Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.override_object_duplicate_move_aggregate"
|
||||
bl_label = "IFC Duplicate Objects Aggregate"
|
||||
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.object_duplicate_move_linked_aggregate"
|
||||
bl_label = "IFC Duplicate Linked Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
|
||||
|
||||
@@ -895,263 +925,103 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
|
||||
|
||||
def _execute(self, context):
|
||||
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_duplicate_linked_aggregate_operator(self, context):
|
||||
self.new_active_obj = None
|
||||
self.group_name = "BBIM_Linked_Aggregate"
|
||||
self.pset_name = "BBIM_Linked_Aggregate"
|
||||
old_to_new = {}
|
||||
|
||||
### Adding the assembly data
|
||||
def add_assembly_data(element, parent, data_to_add):
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Aggregate_Data")
|
||||
data = [data_to_add]
|
||||
def select_objects_and_add_data(element):
|
||||
add_linked_aggregate_group(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj.select_set(True)
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
index = 0
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
select_objects_and_add_data(part)
|
||||
|
||||
if pset:
|
||||
if parent == None:
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=tool.Ifc.get().by_id(pset["id"]),
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=tool.Ifc.get().by_id(pset["id"]),
|
||||
properties={"Parent": parent.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
)
|
||||
add_linked_aggregate_pset(part, index)
|
||||
index += 1
|
||||
|
||||
obj = tool.Ifc.get_object(part)
|
||||
obj.select_set(True)
|
||||
|
||||
else:
|
||||
def add_linked_aggregate_group(element):
|
||||
linked_aggregate_group = None
|
||||
product_groups_name = [
|
||||
r.RelatingGroup.Name
|
||||
for r in getattr(element, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
]
|
||||
if self.group_name in product_groups_name:
|
||||
return
|
||||
|
||||
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name)
|
||||
ifcopenshell.api.run(
|
||||
"group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group
|
||||
)
|
||||
|
||||
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=element, name="BBIM_Aggregate_Data"
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
|
||||
)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Parent": parent.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
)
|
||||
|
||||
def add_child_to_assembly_data(new_entity):
|
||||
pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Aggregate_Data")
|
||||
parent_element = tool.Ifc.get().by_guid(pset["Parent"])
|
||||
parent_pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Aggregate_Data")
|
||||
data = json.loads(parent_pset["Data"])
|
||||
data[0]["children"].append(new_entity.GlobalId)
|
||||
else:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=tool.Ifc.get().by_id(parent_pset["id"]),
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
pset=pset,
|
||||
properties={"Index": index},
|
||||
)
|
||||
|
||||
def create_data_structure(entity, level=-1):
|
||||
level += 1
|
||||
|
||||
data_children = {
|
||||
"children": [],
|
||||
"instance_of": [],
|
||||
}
|
||||
|
||||
data_parent = {
|
||||
"children": [],
|
||||
"instance_of": [entity.GlobalId],
|
||||
}
|
||||
|
||||
if not entity.is_a("IfcElementAssembly"):
|
||||
return
|
||||
else:
|
||||
if level == 0:
|
||||
add_assembly_data(entity, entity, data_parent)
|
||||
else:
|
||||
add_assembly_data(entity, None, data_parent)
|
||||
parts = ifcopenshell.util.element.get_parts(entity)
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
add_assembly_data(part, entity, data_children)
|
||||
add_child_to_assembly_data(part)
|
||||
create_data_structure(part, level)
|
||||
continue
|
||||
add_assembly_data(part, entity, data_children)
|
||||
add_child_to_assembly_data(part)
|
||||
|
||||
return
|
||||
|
||||
def recreate_data_structure(entity, level=-1):
|
||||
level += 1
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(entity, "BBIM_Aggregate_Data")
|
||||
pset_data = json.loads(pset["Data"])[0]
|
||||
instance_of = pset_data["instance_of"]
|
||||
|
||||
data_children = {
|
||||
"children": [],
|
||||
"instance_of": [],
|
||||
}
|
||||
|
||||
# Keeps instance ID through all copies
|
||||
data_parent = {
|
||||
"children": [],
|
||||
"instance_of": instance_of,
|
||||
}
|
||||
|
||||
if not entity.is_a("IfcElementAssembly"):
|
||||
return
|
||||
else:
|
||||
if level == 0:
|
||||
add_assembly_data(entity, entity, data_parent)
|
||||
else:
|
||||
add_assembly_data(entity, None, data_parent)
|
||||
parts = ifcopenshell.util.element.get_parts(entity)
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
add_child_to_assembly_data(part)
|
||||
recreate_data_structure(part, level)
|
||||
continue
|
||||
add_assembly_data(part, entity, data_children)
|
||||
add_child_to_assembly_data(part)
|
||||
|
||||
return
|
||||
|
||||
def duplicate_all(obj, level=-1, new_parent=None, parents=[]):
|
||||
level += 1
|
||||
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
|
||||
if level == 0:
|
||||
new_parent = duplicate_objects(obj)
|
||||
parents.append(new_parent)
|
||||
else:
|
||||
pair = []
|
||||
pair.append(new_parent)
|
||||
new_parent = duplicate_objects(obj)
|
||||
pair.append(new_parent)
|
||||
parents.append(pair)
|
||||
|
||||
if not entity.is_a("IfcElementAssembly"):
|
||||
return
|
||||
else:
|
||||
parts = ifcopenshell.util.element.get_parts(entity)
|
||||
|
||||
# Ensures that we are duplication all the IfcElementAssembly
|
||||
# before duplicating the nested objects
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
else:
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
new_part = duplicate_objects(part_obj)
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(new_parent),
|
||||
related_obj=tool.Ifc.get_object(new_part),
|
||||
)
|
||||
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
|
||||
# Recursion Call
|
||||
duplicate_all(part_obj, level, new_parent, parents)
|
||||
|
||||
if level == 0:
|
||||
for p in parents[1:]:
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(p[0]),
|
||||
related_obj=tool.Ifc.get_object(p[1]),
|
||||
)
|
||||
|
||||
return new_parent
|
||||
return
|
||||
|
||||
def duplicate_objects(obj, is_root=False):
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
self.new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
|
||||
# This is needed to make sure the new object gets unlink from
|
||||
# the old object assembly collection
|
||||
new_obj.BIMObjectProperties.collection = None
|
||||
|
||||
# Copy the actual class
|
||||
new_entity = blenderbim.core.root.copy_class(
|
||||
tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj
|
||||
)
|
||||
|
||||
if new_entity:
|
||||
tool.Model.handle_array_on_copied_element(new_entity)
|
||||
blenderbim.core.aggregate.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(selected_root_entity),
|
||||
related_obj=tool.Ifc.get_object(new_entity),
|
||||
)
|
||||
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = [new_entity]
|
||||
|
||||
return new_entity
|
||||
|
||||
### Check if only one element and it's assembly
|
||||
return index
|
||||
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
return {"FINISHED"}
|
||||
|
||||
selected_obj = context.selected_objects[0]
|
||||
selected_root_entity = tool.Ifc.get_entity(selected_obj)
|
||||
selected_element = tool.Ifc.get_entity(selected_obj)
|
||||
|
||||
if not selected_root_entity.is_a("IfcElementAssembly"):
|
||||
if selected_element.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
elif selected_element.Decomposes:
|
||||
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_element = selected_element.Decomposes[0].RelatingObject
|
||||
selected_obj = tool.Ifc.get_object(selected_element)
|
||||
else:
|
||||
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
|
||||
return {"FINISHED"}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
|
||||
if not pset:
|
||||
create_data_structure(selected_root_entity)
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
|
||||
select_objects_and_add_data(selected_element)
|
||||
|
||||
selected_root_parent = selected_root_entity
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
new_root_entity = duplicate_all(selected_obj)
|
||||
|
||||
# Recreate aggregate relationship
|
||||
for old in old_to_new.keys():
|
||||
if old.is_a("IfcElementAssembly"):
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
recreate_data_structure(new_root_entity)
|
||||
|
||||
# Remove connections with old objects
|
||||
for new in old_to_new.values():
|
||||
for connection in new[0].ConnectedTo:
|
||||
entity = connection.RelatedElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
for connection in new[0].ConnectedFrom:
|
||||
entity = connection.RelatingElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
|
||||
old_objs = []
|
||||
for old, new in old_to_new.items():
|
||||
old_objs.append(tool.Ifc.get_object(old))
|
||||
|
||||
relationships = tool.Root.get_decomposition_relationships(old_objs)
|
||||
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
return {"FINISHED"}
|
||||
return old_to_new
|
||||
|
||||
|
||||
class RefreshAggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.refresh_aggregate"
|
||||
bl_label = "IFC Refresh Aggregate"
|
||||
class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.refresh_linked_aggregate"
|
||||
bl_label = "IFC Refresh Linked Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
@@ -1167,214 +1037,132 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
refresh_start_time = time()
|
||||
self.new_active_obj = None
|
||||
self.group_name = 'BBIM_Linked_Aggregate'
|
||||
self.pset_name = "BBIM_Linked_Aggregate"
|
||||
refresh_start_time = time()
|
||||
old_to_new = {}
|
||||
|
||||
def remove_objects(entity, level=-1, parents=[]):
|
||||
level += 1
|
||||
if level == 0:
|
||||
parents = [entity]
|
||||
parts = ifcopenshell.util.element.get_parts(entity)
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
parents.append(part)
|
||||
remove_objects(part, level, parents)
|
||||
continue
|
||||
else:
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
if part_obj:
|
||||
tool.Geometry.delete_ifc_object(part_obj)
|
||||
def delete_objects(element):
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
delete_objects(part)
|
||||
|
||||
tool.Geometry.delete_ifc_object(tool.Ifc.get_object(part))
|
||||
|
||||
tool.Geometry.delete_ifc_object(tool.Ifc.get_object(element))
|
||||
|
||||
return parents
|
||||
|
||||
def duplicate_children(entity):
|
||||
pset = ifcopenshell.util.element.get_pset(entity, "BBIM_Aggregate_Data")
|
||||
pset_data = json.loads(pset["Data"])[0]
|
||||
instance_of = pset_data["instance_of"][0]
|
||||
|
||||
instance_entity = tool.Ifc.get().by_guid(instance_of)
|
||||
|
||||
if not instance_entity.is_a("IfcElementAssembly"):
|
||||
return
|
||||
def get_element_assembly(element):
|
||||
if element.is_a("IfcElementAssembly"):
|
||||
return element
|
||||
elif element.Decomposes:
|
||||
if element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
element = element.Decomposes[0].RelatingObject
|
||||
return element
|
||||
else:
|
||||
parts = ifcopenshell.util.element.get_parts(instance_entity)
|
||||
for part in parts:
|
||||
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
if not pset:
|
||||
data_children = {
|
||||
"children": [],
|
||||
"instance_of": [part.GlobalId],
|
||||
}
|
||||
data = [data_children]
|
||||
return None
|
||||
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data"
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={
|
||||
"Parent": instance_entity.GlobalId,
|
||||
"Data": tool.Ifc.get().createIfcText(json.dumps(data)),
|
||||
},
|
||||
)
|
||||
def handle_selection(selected_objs):
|
||||
selected_elements = [tool.Ifc.get_entity(selected_obj) for selected_obj in selected_objs]
|
||||
if None in selected_elements:
|
||||
self.report({"INFO"}, "Object has no Ifc Metadata.")
|
||||
return None, None
|
||||
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
new_part = duplicate_objects(part_obj)
|
||||
selected_parents = []
|
||||
for selected_element in selected_elements:
|
||||
selected_element = get_element_assembly(selected_element)
|
||||
if not selected_element:
|
||||
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
|
||||
return None, None
|
||||
selected_parents.append(selected_element)
|
||||
|
||||
selected_parents = list(set(selected_parents))
|
||||
linked_aggregate_groups = []
|
||||
for selected_element in selected_parents:
|
||||
product_linked_agg_group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(selected_element, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if self.group_name in r.RelatingGroup.Name
|
||||
]
|
||||
try:
|
||||
linked_aggregate_groups.append(product_linked_agg_group[0].id())
|
||||
except:
|
||||
self.report({"INFO"}, "Object is not part of a Linked Aggregate.")
|
||||
return None, None
|
||||
|
||||
return list(set(linked_aggregate_groups)), selected_parents
|
||||
|
||||
active_element = tool.Ifc.get_entity(context.active_object)
|
||||
if not active_element:
|
||||
self.report({"INFO"}, "Object has no Ifc metadata.")
|
||||
return {"FINISHED"}
|
||||
|
||||
active_element = get_element_assembly(active_element)
|
||||
selected_objs = context.selected_objects
|
||||
linked_aggregate_groups, selected_parents = handle_selection(selected_objs)
|
||||
if not linked_aggregate_groups or not selected_parents:
|
||||
return {"FINISHED"}
|
||||
|
||||
if len(linked_aggregate_groups) > 1:
|
||||
if len(selected_parents) != len(linked_aggregate_groups):
|
||||
self.report({"INFO"}, "Select only one object from each Linked Aggregate or multiple objects from the same Linked Aggregate.")
|
||||
return {"FINISHED"}
|
||||
|
||||
for group in linked_aggregate_groups:
|
||||
elements = tool.Drawing.get_group_elements(tool.Ifc.get().by_id(group))
|
||||
if len(linked_aggregate_groups) > 1:
|
||||
base_instance = [e for e in elements if e in selected_parents][0]
|
||||
instances_to_refresh = elements
|
||||
|
||||
elif (len(linked_aggregate_groups) == 1) and (len(selected_parents) > 1):
|
||||
base_instance = active_element
|
||||
instances_to_refresh = [element for element in elements if element in selected_parents]
|
||||
|
||||
else:
|
||||
base_instance = active_element
|
||||
instances_to_refresh = elements
|
||||
|
||||
for element in instances_to_refresh:
|
||||
if element.GlobalId == base_instance.GlobalId:
|
||||
continue
|
||||
|
||||
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()
|
||||
|
||||
delete_objects(element)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
|
||||
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
|
||||
|
||||
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(entity),
|
||||
related_obj=tool.Ifc.get_object(new_part),
|
||||
)
|
||||
duplicate_children(new_part)
|
||||
|
||||
for part in parts:
|
||||
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
|
||||
else:
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data"
|
||||
)
|
||||
else:
|
||||
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
|
||||
data_children = {
|
||||
"children": [],
|
||||
"instance_of": [],
|
||||
}
|
||||
data = [data_children]
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={
|
||||
"Parent": instance_entity.GlobalId,
|
||||
"Data": tool.Ifc.get().createIfcText(json.dumps(data)),
|
||||
},
|
||||
)
|
||||
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
new_part = duplicate_objects(part_obj)
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(entity),
|
||||
related_obj=tool.Ifc.get_object(new_part),
|
||||
)
|
||||
|
||||
def duplicate_objects(obj, is_root=False):
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
self.new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
|
||||
# This is needed to make sure the new object gets unlink from
|
||||
# the old object assembly collection
|
||||
new_obj.BIMObjectProperties.collection = None
|
||||
|
||||
# Copy the actual class
|
||||
new_entity = blenderbim.core.root.copy_class(
|
||||
tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj
|
||||
)
|
||||
|
||||
if new_entity:
|
||||
tool.Model.handle_array_on_copied_element(new_entity)
|
||||
|
||||
if not new_entity.is_a("IfcElementAssembly"):
|
||||
blenderbim.core.aggregate.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=obj,
|
||||
related_obj=tool.Ifc.get_object(new_entity),
|
||||
)
|
||||
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = [new_entity]
|
||||
|
||||
return new_entity
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
self.report({"INFO"}, "Only 1 object need to be selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
selected_root_obj = context.selected_objects[0]
|
||||
selected_root_entity = tool.Ifc.get_entity(selected_root_obj)
|
||||
|
||||
if selected_root_entity.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
elif selected_root_entity.Decomposes:
|
||||
if selected_root_entity.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_root_entity = selected_root_entity.Decomposes[0].RelatingObject
|
||||
selected_root_obj = tool.Ifc.get_object(selected_root_entity)
|
||||
else:
|
||||
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
|
||||
return {"FINISHED"}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
|
||||
if not pset:
|
||||
self.report({"INFO"}, "Object is not part of an assembly aggregate.")
|
||||
return {"FINISHED"}
|
||||
|
||||
pset_data = json.loads(pset["Data"])[0]
|
||||
instance_of = pset_data["instance_of"][0]
|
||||
original_root_entity = tool.Ifc.get().by_guid(instance_of)
|
||||
if original_root_entity == selected_root_entity:
|
||||
self.report({"INFO"}, "Cannot refresh original assembly. Select an assembly instance.")
|
||||
return {"FINISHED"}
|
||||
|
||||
parents = remove_objects(selected_root_entity)
|
||||
|
||||
original_root_object = tool.Ifc.get_object(original_root_entity)
|
||||
|
||||
selected_matrix = selected_root_obj.matrix_world
|
||||
original_matrix = original_root_object.matrix_world
|
||||
|
||||
for parent in parents:
|
||||
duplicate_children(parent)
|
||||
|
||||
# Remove connections with old objects
|
||||
for new in old_to_new.values():
|
||||
for connection in new[0].ConnectedTo:
|
||||
entity = connection.RelatedElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
for connection in new[0].ConnectedFrom:
|
||||
entity = connection.RelatingElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
|
||||
old_objs = []
|
||||
for old, new in old_to_new.items():
|
||||
old_objs.append(tool.Ifc.get_object(old))
|
||||
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
|
||||
matrix_diff = Matrix.inverted(original_matrix) @ new_obj.matrix_world
|
||||
new_matrix = selected_matrix @ matrix_diff
|
||||
|
||||
new_obj.matrix_world = new_matrix
|
||||
|
||||
relationships = tool.Root.get_decomposition_relationships(old_objs)
|
||||
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
operator_time = time() - refresh_start_time
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel, Menu, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
@@ -53,6 +54,22 @@ class BIM_MT_separate(Menu):
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE"
|
||||
|
||||
|
||||
class BIM_MT_hotkey_separate(Menu):
|
||||
bl_idname = "BIM_MT_hotkey_separate"
|
||||
bl_label = "Separate"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text="IFC Separate", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("bim.override_mesh_separate", text="Selection").type = "SELECTED"
|
||||
self.layout.operator("bim.override_mesh_separate", text="By Material").type = "MATERIAL"
|
||||
self.layout.operator("bim.override_mesh_separate", text="By Loose Parts").type = "LOOSE"
|
||||
self.layout.separator()
|
||||
self.layout.label(text="Blender Separate", icon="BLENDER")
|
||||
self.layout.operator("mesh.separate", text="Selection").type = "SELECTED"
|
||||
self.layout.operator("mesh.separate", text="By Material").type = "MATERIAL"
|
||||
self.layout.operator("mesh.separate", text="By Loose Parts").type = "LOOSE"
|
||||
|
||||
|
||||
class BIM_MT_object_set_origin(Menu):
|
||||
bl_idname = "BIM_MT_object_set_origin"
|
||||
bl_label = "IFC Set Origin"
|
||||
@@ -164,17 +181,21 @@ class BIM_PT_representation_items(Panel):
|
||||
|
||||
self.layout.template_list("BIM_UL_representation_items", "", props, "items", props, "active_item_index")
|
||||
|
||||
item_is_active = props.active_item_index < len(props.items)
|
||||
if item_is_active:
|
||||
surface_style = props.items[props.active_item_index].surface_style
|
||||
presentation_layer = props.items[props.active_item_index].layer
|
||||
else:
|
||||
surface_style, presentation_layer = None, None
|
||||
|
||||
row = self.layout.row()
|
||||
if RepresentationItemsData.data["active_surface_style"]:
|
||||
row.label(text=RepresentationItemsData.data["active_surface_style"], icon="MATERIAL")
|
||||
if surface_style:
|
||||
row.label(text=surface_style, icon="SHADING_RENDERED")
|
||||
else:
|
||||
row.label(text="No Surface Style", icon="MESH_UVSPHERE")
|
||||
|
||||
row = self.layout.row()
|
||||
if RepresentationItemsData.data["active_layer"]:
|
||||
row.label(text=RepresentationItemsData.data["active_layer"], icon="STICKY_UVS_LOC")
|
||||
else:
|
||||
row.label(text="No Presentation Layer", icon="STICKY_UVS_LOC")
|
||||
row.label(text=presentation_layer or "No Presentation Layer", icon="STICKY_UVS_LOC")
|
||||
|
||||
|
||||
class BIM_PT_connections(Panel):
|
||||
@@ -335,20 +356,26 @@ class BIM_PT_placement(Panel):
|
||||
if not PlacementData.is_loaded:
|
||||
PlacementData.load()
|
||||
|
||||
if PlacementData.data["has_placement"]:
|
||||
row = self.layout.row()
|
||||
row.prop(context.active_object, "location", text="Location")
|
||||
row = self.layout.row()
|
||||
row.prop(context.active_object, "rotation_euler", text="Rotation")
|
||||
else:
|
||||
if not PlacementData.data["has_placement"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Object Placement Found")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(context.active_object, "location", text="Location")
|
||||
row = self.layout.row()
|
||||
row.prop(context.active_object, "rotation_euler", text="Rotation")
|
||||
|
||||
if context.active_object.BIMObjectProperties.blender_offset_type != "NONE":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS")
|
||||
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=PlacementData.data["original_x"], icon="EMPTY_AXIS")
|
||||
row.label(text=PlacementData.data["original_y"])
|
||||
row.label(text=PlacementData.data["original_z"])
|
||||
|
||||
|
||||
class BIM_PT_derived_coordinates(Panel):
|
||||
bl_label = "Derived Coordinates"
|
||||
|
||||
@@ -37,7 +37,7 @@ classes = (
|
||||
operator.ImportPlot,
|
||||
prop.BIMGeoreferenceProperties,
|
||||
ui.BIM_PT_gis,
|
||||
ui.BIM_PT_gis_utilities,
|
||||
ui.BIM_PT_gis_calculator,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -151,13 +151,14 @@ class BIM_PT_gis(Panel):
|
||||
row.label(text=GeoreferenceData.data["true_derived_angle"])
|
||||
|
||||
|
||||
class BIM_PT_gis_utilities(Panel):
|
||||
bl_idname = "BIM_PT_gis_utilities"
|
||||
bl_label = "Georeferencing Utilities"
|
||||
class BIM_PT_gis_calculator(Panel):
|
||||
bl_idname = "BIM_PT_gis_calculator"
|
||||
bl_label = "Georeferencing Calculator"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "BlenderBIM"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_gis"
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
|
||||
@@ -29,7 +29,6 @@ classes = (
|
||||
operator.LoadGroups,
|
||||
operator.RemoveGroup,
|
||||
operator.SelectGroupProducts,
|
||||
operator.ToggleAssigningGroup,
|
||||
operator.ToggleGroup,
|
||||
operator.UnassignGroup,
|
||||
operator.UpdateGroup,
|
||||
|
||||
@@ -173,16 +173,6 @@ class DisableEditingGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleAssigningGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.toggle_assigning_group"
|
||||
bl_label = "Toggle Assigning Group"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
context.scene.BIMGroupProperties.is_adding = not context.scene.BIMGroupProperties.is_adding
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AssignGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_group"
|
||||
bl_label = "Assign Group"
|
||||
|
||||
@@ -49,7 +49,6 @@ class Group(PropertyGroup):
|
||||
class BIMGroupProperties(PropertyGroup):
|
||||
group_attributes: CollectionProperty(name="Group Attributes", type=Attribute)
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
is_adding: BoolProperty(name="Is Adding", default=False)
|
||||
groups: CollectionProperty(name="Groups", type=Group)
|
||||
active_group_index: IntProperty(name="Active Group Index")
|
||||
active_group_id: IntProperty(name="Active Group Id")
|
||||
|
||||
@@ -89,9 +89,9 @@ class BIM_PT_object_groups(Panel):
|
||||
ObjectGroupsData.load()
|
||||
self.props = context.scene.BIMGroupProperties
|
||||
row = self.layout.row(align=True)
|
||||
if self.props.is_adding:
|
||||
if self.props.is_editing:
|
||||
row.label(text="Adding Groups", icon="OUTLINER")
|
||||
row.operator("bim.toggle_assigning_group", text="", icon="CANCEL")
|
||||
row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
|
||||
self.layout.template_list(
|
||||
"BIM_UL_object_groups",
|
||||
"",
|
||||
@@ -102,7 +102,7 @@ class BIM_PT_object_groups(Panel):
|
||||
)
|
||||
else:
|
||||
row.label(text=f"{ObjectGroupsData.data['total_groups']} Groups in IFC Project", icon="OUTLINER")
|
||||
row.operator("bim.toggle_assigning_group", text="", icon="ADD")
|
||||
row.operator("bim.load_groups", text="", icon="GREASEPENCIL")
|
||||
|
||||
for group in ObjectGroupsData.data["groups"]:
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -96,7 +96,7 @@ class DiscardUncommitted(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
core.discard_uncomitted(tool.IfcGit, tool.Ifc)
|
||||
core.discard_uncommitted(tool.IfcGit, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -115,18 +115,17 @@ class CommitChanges(bpy.types.Operator):
|
||||
repo = IfcGitData.data["repo"]
|
||||
if props.commit_message == "":
|
||||
return False
|
||||
if (
|
||||
repo
|
||||
and repo.head.is_detached
|
||||
and (
|
||||
not tool.IfcGit.is_valid_ref_format(props.new_branch_name)
|
||||
or props.new_branch_name in [branch.name for branch in repo.branches]
|
||||
)
|
||||
):
|
||||
cls.poll_message_set(
|
||||
"The new branch name is invalid, please insert a valid branch name (eg. with no spaces, ...)"
|
||||
)
|
||||
return False
|
||||
if repo:
|
||||
if props.new_branch_name in [branch.name for branch in repo.branches]:
|
||||
cls.poll_message_set("Branch already exists!")
|
||||
return False
|
||||
elif not tool.IfcGit.is_valid_ref_format(props.new_branch_name):
|
||||
if repo.head.is_detached:
|
||||
cls.poll_message_set("Branch name is invalid or empty!")
|
||||
return False
|
||||
elif props.new_branch_name != "":
|
||||
cls.poll_message_set("Branch name is invalid!")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
@@ -79,10 +79,12 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
||||
row = layout.row()
|
||||
row.prop(props, "commit_message")
|
||||
|
||||
row = layout.row()
|
||||
if IfcGitData.data["is_detached"]:
|
||||
row = layout.row()
|
||||
row.label(text="HEAD is detached, commit will create a branch", icon="ERROR")
|
||||
row.prop(props, "new_branch_name")
|
||||
else:
|
||||
row.label(text="Optionally create a branch:")
|
||||
row.prop(props, "new_branch_name")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("ifcgit.commit_changes", icon="GREASEPENCIL")
|
||||
|
||||
@@ -108,6 +108,7 @@ classes = (
|
||||
space.GenerateSpace,
|
||||
space.GenerateSpacesFromWalls,
|
||||
covering.AddInstanceFlooringCoveringsFromWalls,
|
||||
covering.AddInstanceCeilingCoveringsFromWalls,
|
||||
covering.AddInstanceFlooringCoveringFromCursor,
|
||||
covering.AddInstanceCeilingCoveringFromCursor,
|
||||
covering.RegenSelectedCoveringObject,
|
||||
@@ -209,12 +210,7 @@ def register():
|
||||
bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
|
||||
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
|
||||
bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(window.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(railing.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(roof.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(ui.add_mesh_object_menu)
|
||||
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
wm = bpy.context.window_manager
|
||||
@@ -245,12 +241,7 @@ def unregister():
|
||||
del bpy.types.Object.BIMRailingProperties
|
||||
del bpy.types.Object.BIMRoofProperties
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(railing.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(roof.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(ui.add_mesh_object_menu)
|
||||
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
|
||||
wm = bpy.context.window_manager
|
||||
kc = wm.keyconfigs.addon
|
||||
|
||||
@@ -189,7 +189,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
if not self.keep_objs:
|
||||
data[self.item]["count"] = 1
|
||||
tool.Model.regenerate_array(parent, data, self.keep_objs)
|
||||
tool.Model.regenerate_array(parent, data, array_layers_to_apply=[self.item] if self.keep_objs else [])
|
||||
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
if len(data) == 1:
|
||||
@@ -248,21 +248,30 @@ class SelectAllArrayObjects(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
object = context.active_object
|
||||
element = tool.Ifc.get_entity(object)
|
||||
array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not array_pset:
|
||||
self.report({"ERROR"}, f"Object is not part of an array.")
|
||||
return {"CANCELLED"}
|
||||
objects = context.selected_objects
|
||||
for object in objects:
|
||||
element = tool.Ifc.get_entity(object)
|
||||
if not element:
|
||||
self.report({"ERROR"}, f"Non IFC objects, were deselected.")
|
||||
object.select_set(False)
|
||||
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
|
||||
except RuntimeError:
|
||||
self.report({"ERROR"}, f"Couldn't find array parent by guid '{array_pset['Parent']}'")
|
||||
return {"CANCELLED"}
|
||||
if element:
|
||||
array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not array_pset:
|
||||
self.report({"ERROR"}, f"Objects not part of an array, were deselected.")
|
||||
object.select_set(False)
|
||||
|
||||
array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element)
|
||||
tool.Blender.set_objects_selection(context, active_object=array_objects[0], selected_objects=array_objects)
|
||||
if array_pset:
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
|
||||
except RuntimeError:
|
||||
self.report({"ERROR"}, f"Objects that don't have an array parent, were deselected.")
|
||||
object.select_set(False)
|
||||
|
||||
array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element)
|
||||
tool.Blender.set_objects_selection(
|
||||
context, active_object=array_objects[0], selected_objects=array_objects, clear_previous_selection=False
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -145,3 +145,42 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
|
||||
|
||||
core.add_instance_flooring_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry)
|
||||
|
||||
class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_instance_ceiling_coverings_from_walls"
|
||||
bl_label = "Add Ceilings 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"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element:
|
||||
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
# walls (i.e. prismatic) in the active object storey.
|
||||
# In order to run, the active object must be a wall and
|
||||
# there must be selected walls
|
||||
|
||||
active_obj = bpy.context.active_object
|
||||
if not active_obj:
|
||||
self.report({"ERROR"}, "No active object. Please select a wall")
|
||||
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.")
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if not container:
|
||||
self.report({"ERROR"}, "The wall is not contained.")
|
||||
|
||||
if not bpy.context.selected_objects:
|
||||
self.report({"ERROR"}, "No selected objects found. Please select walls.")
|
||||
return
|
||||
|
||||
core.add_instance_ceiling_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry, tool.Covering)
|
||||
|
||||
|
||||
@@ -255,6 +255,8 @@ class AuthoringData:
|
||||
if not relating_type_id:
|
||||
return
|
||||
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
if not hasattr(relating_type, "PredefinedType"):
|
||||
return
|
||||
predefined_type = relating_type.PredefinedType
|
||||
return predefined_type
|
||||
|
||||
@@ -268,7 +270,9 @@ class AuthoringData:
|
||||
usage = tool.Model.get_usage_type(element)
|
||||
if not usage:
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
if representation and representation.RepresentationType == "SweptSolid":
|
||||
# besides IfcRepresentation it could be IfcCurveBoundedPlane
|
||||
# if IfcRelSpaceBoundary selected
|
||||
if representation and getattr(representation, "RepresentationType", None) == "SweptSolid":
|
||||
usage = "SWEPTSOLID"
|
||||
else:
|
||||
continue
|
||||
@@ -314,6 +318,7 @@ class StairData:
|
||||
if not cls.data["pset_data"]:
|
||||
return
|
||||
cls.data["general_params"] = cls.general_params()
|
||||
cls.data["calculated_params"] = cls.calculated_params()
|
||||
|
||||
@classmethod
|
||||
def pset_data(cls):
|
||||
@@ -330,6 +335,10 @@ class StairData:
|
||||
general_params[prop_readable_name] = prop_value
|
||||
return general_params
|
||||
|
||||
@classmethod
|
||||
def calculated_params(cls):
|
||||
return tool.Model.get_active_stair_calculated_params(cls.data["pset_data"]["data_dict"])
|
||||
|
||||
|
||||
class SverchokData:
|
||||
data = {}
|
||||
@@ -356,7 +365,7 @@ class SverchokData:
|
||||
|
||||
|
||||
def get_prop_from_data(props, data, prop_name):
|
||||
prop_value = data[prop_name]
|
||||
prop_value = data.get(prop_name, tool.Blender.get_blender_prop_default_value(props, prop_name))
|
||||
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
|
||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||
return prop_readable_name, prop_value
|
||||
|
||||
@@ -191,15 +191,15 @@ class ProfileDecorator:
|
||||
unselected_edges.append(edge_indices)
|
||||
|
||||
### Actually drawing
|
||||
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
|
||||
# Draw faces
|
||||
|
||||
@@ -647,7 +647,3 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_door.bl_idname, icon="PLUGIN")
|
||||
|
||||
@@ -111,7 +111,3 @@ class BIM_OT_add_object(Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
add_object(self, context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN")
|
||||
|
||||
@@ -114,10 +114,8 @@ class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj_pred = tool.Ifc.get_object(predecessor)
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
if tool.Ifc.is_moved(obj_pred):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj_pred)
|
||||
tool.Model.sync_object_ifc_position(obj)
|
||||
tool.Model.sync_object_ifc_position(obj_pred)
|
||||
|
||||
port, port_pred = get_connected_ports_between(element, predecessor)
|
||||
port_matrix_pred = tool.Model.get_element_matrix(port_pred)
|
||||
@@ -311,6 +309,7 @@ class MEPGenerator:
|
||||
profile_joiner.set_depth(connected_obj, connected_element_length)
|
||||
|
||||
def get_segment_data(self, segment):
|
||||
"""returns points data is in world space"""
|
||||
ports = tool.System.get_ports(segment)
|
||||
segment_object = tool.Ifc.get_object(segment)
|
||||
start_point = segment_object.location
|
||||
@@ -451,11 +450,12 @@ class MEPGenerator:
|
||||
# NOTE: I have a feeling that there are cases where order
|
||||
# in which we're checking the segments is important
|
||||
# but I couldn't pin it down to exact cases
|
||||
for test_segment_data in fitting_data[:]:
|
||||
for test_segment_data in fitting_data:
|
||||
for base_segment_data in segments_data:
|
||||
if not are_segments_compatible(test_segment_data, base_segment_data):
|
||||
continue
|
||||
segments_data.remove(test_segment_data)
|
||||
segments_data.remove(base_segment_data)
|
||||
break
|
||||
|
||||
# all segments were sorted
|
||||
return len(segments_data) == 0
|
||||
@@ -988,11 +988,13 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
start_object_rotation = start_object.matrix_world.to_quaternion().to_matrix()
|
||||
start_segment_data = MEPGenerator().get_segment_data(start_element)
|
||||
end_segment_data = MEPGenerator().get_segment_data(end_element)
|
||||
# use id() to match by the exact vector objects and not by their values
|
||||
# since vectors position could match
|
||||
points_ports_map = {
|
||||
start_segment_data["start_point"]: start_segment_data["start_port"],
|
||||
start_segment_data["end_point"]: start_segment_data["end_port"],
|
||||
end_segment_data["start_point"]: end_segment_data["start_port"],
|
||||
end_segment_data["end_point"]: end_segment_data["end_port"],
|
||||
id(start_segment_data["start_point"]): start_segment_data["start_port"],
|
||||
id(start_segment_data["end_point"]): start_segment_data["end_port"],
|
||||
id(end_segment_data["start_point"]): end_segment_data["start_port"],
|
||||
id(end_segment_data["end_point"]): end_segment_data["end_port"],
|
||||
}
|
||||
|
||||
get_z_basis = lambda o: tool.Cad.get_basis_vector(o, 2)
|
||||
@@ -1008,8 +1010,10 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
segments_intersection_ws, (end_segment_data["start_point"], end_segment_data["end_point"])
|
||||
)
|
||||
|
||||
start_port = points_ports_map[start_point]
|
||||
end_port = points_ports_map[end_point]
|
||||
# start_/end_segment_sign indicate
|
||||
# whether segments' z axes are directed towards the bend
|
||||
start_port = points_ports_map[id(start_point)]
|
||||
end_port = points_ports_map[id(end_point)]
|
||||
start_point_on_origin = start_point == start_segment_data["start_point"]
|
||||
start_connection = "ATSTART" if start_point_on_origin else "ATEND"
|
||||
start_segment_sign = -1 if start_point_on_origin else 1
|
||||
@@ -1022,7 +1026,14 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# TODO: profile offset may need to be flipped (check transition code)
|
||||
to_start_object_space = start_object_rotation.inverted()
|
||||
profile_offset = (to_start_object_space @ end_point) - (to_start_object_space @ start_point)
|
||||
ref_point = end_point.copy()
|
||||
end_segment_dir = (second_segment_end - end_point).normalized()
|
||||
# we prioritize direction between end_point and start_point for bend_vector
|
||||
# if those point match we use general end segment direction
|
||||
if tool.Cad.is_x((end_point - start_point).length, 0):
|
||||
ref_point = end_point + end_segment_dir
|
||||
bend_vector = (to_start_object_space @ ref_point) - (to_start_object_space @ start_point)
|
||||
|
||||
z_axis_end_object_local = to_start_object_space @ tool.Cad.get_basis_vector(end_object, 2)
|
||||
|
||||
def check_for_double_bends():
|
||||
@@ -1046,7 +1057,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
non_lateral_axis = 0 if lateral_axes[0] == 1 else 1
|
||||
non_lateral_axis_offset = profile_offset[non_lateral_axis]
|
||||
non_lateral_axis_offset = bend_vector[non_lateral_axis]
|
||||
if not tool.Cad.is_x(non_lateral_axis_offset, 0):
|
||||
return (
|
||||
None,
|
||||
@@ -1072,29 +1083,27 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
angle, rotation_axis = get_bend_rotation()
|
||||
|
||||
lateral_sign = tool.Cad.sign(profile_offset[lateral_axis])
|
||||
lateral_sign = tool.Cad.sign(bend_vector[lateral_axis])
|
||||
radial_offset = V(0, 0, 0)
|
||||
ref_point_radius = self.radius + profile_dim[lateral_axis]
|
||||
radial_offset[lateral_axis] = ref_point_radius * (1 - cos(angle)) * lateral_sign
|
||||
radial_offset.z = ref_point_radius * sin(angle)
|
||||
second_object_offset = radial_offset + V(0, 0, self.start_length)
|
||||
second_object_offset += z_axis_end_object_local * (self.end_length * -end_segment_sign)
|
||||
|
||||
def get_segments_extend():
|
||||
segments_intersection = segments_intersection_ws - start_point
|
||||
segments_intersection = to_start_object_space @ segments_intersection
|
||||
radial_offset.z = ref_point_radius * sin(angle) * start_segment_sign
|
||||
end_port_offset = radial_offset + V(0, 0, self.start_length * start_segment_sign)
|
||||
end_port_offset += z_axis_end_object_local * (self.end_length * -end_segment_sign)
|
||||
|
||||
def get_segments_extend_points():
|
||||
# since tangent segments are equal
|
||||
# if drawn for the circle from the same point
|
||||
required_offset = ref_point_radius * tan(angle / 2)
|
||||
|
||||
current_start_offset = segments_intersection.length
|
||||
current_end_offset = (segments_intersection - profile_offset).length
|
||||
start_segment_extend_point = segments_intersection_ws - start_segment_sign * (
|
||||
self.start_length + required_offset
|
||||
) * get_z_basis(start_object)
|
||||
end_segment_extend_point = segments_intersection_ws - end_segment_sign * (
|
||||
self.end_length + required_offset
|
||||
) * get_z_basis(end_object)
|
||||
|
||||
start_extend = current_start_offset - (required_offset + self.start_length)
|
||||
end_extend = current_end_offset - (required_offset + self.end_length)
|
||||
|
||||
return start_extend, end_extend
|
||||
return start_segment_extend_point, end_segment_extend_point
|
||||
|
||||
def check_new_segment_length(start_point, end_point, extend_point):
|
||||
"""Check if segment is placed too near to the bend point.
|
||||
@@ -1114,8 +1123,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return None
|
||||
|
||||
# adjust segments to fit the radius and angle
|
||||
start_segment_extend, end_segment_extend = get_segments_extend()
|
||||
start_segment_extend_point = start_point + start_segment_sign * start_segment_extend * get_z_basis(start_object)
|
||||
start_segment_extend_point, end_segment_extend_point = get_segments_extend_points()
|
||||
projection = check_new_segment_length(first_segment_start, start_point, start_segment_extend_point)
|
||||
if projection is not None:
|
||||
self.report(
|
||||
@@ -1124,7 +1132,6 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
return {"ERROR"}
|
||||
|
||||
end_segment_extend_point = end_point + end_segment_sign * end_segment_extend * get_z_basis(end_object)
|
||||
projection = check_new_segment_length(second_segment_end, end_point, end_segment_extend_point)
|
||||
if projection is not None:
|
||||
self.report(
|
||||
@@ -1145,7 +1152,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.end_length / si_conversion,
|
||||
angle,
|
||||
self.radius / si_conversion,
|
||||
profile_offset / si_conversion,
|
||||
bend_vector / si_conversion,
|
||||
flip_z_axis=start_segment_sign == -1,
|
||||
)
|
||||
|
||||
@@ -1202,7 +1209,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset=pset,
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(bend_data, default=list))},
|
||||
)
|
||||
tool.System.add_ports(obj, end_port_pos=second_object_offset)
|
||||
tool.System.add_ports(obj, end_port_pos=end_port_offset)
|
||||
|
||||
# NOTE: at this point we loose current blender objects selection
|
||||
# create transition element
|
||||
|
||||
@@ -181,7 +181,7 @@ class FilledOpeningGenerator:
|
||||
return
|
||||
|
||||
opening = filling.FillsVoids[0].RelatingOpeningElement
|
||||
voided_obj = tool.Ifc.get_object(opening.VoidsElements[0].RelatingBuildingElement)
|
||||
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
||||
|
||||
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||
ifcopenshell.api.run(
|
||||
@@ -217,16 +217,22 @@ class FilledOpeningGenerator:
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
)
|
||||
|
||||
representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=voided_obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
# update voided object representation or all it's parts if it's an aggregate
|
||||
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
|
||||
for voided_element in voided_elements:
|
||||
voided_obj = tool.Ifc.get_object(voided_element)
|
||||
if not voided_obj.data:
|
||||
continue
|
||||
representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=voided_obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
def generate_opening_from_filling(self, filling, filling_obj):
|
||||
# Since openings are reused later, we give a default thickness of 1.2m
|
||||
@@ -732,6 +738,9 @@ class HideOpenings(Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
element = element.VoidsElements[0].RelatingBuildingElement
|
||||
obj = tool.Ifc.get_object(element)
|
||||
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
|
||||
for opening in openings:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
@@ -759,10 +768,14 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
element = element.VoidsElements[0].RelatingBuildingElement
|
||||
obj = tool.Ifc.get_object(element)
|
||||
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
|
||||
for opening in openings:
|
||||
similar_openings = [o for o in all_openings if o.ObjectPlacement == opening.ObjectPlacement]
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
building_objs.add(obj)
|
||||
if opening_obj:
|
||||
if tool.Ifc.is_edited(opening_obj):
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
@@ -770,9 +783,12 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
blenderbim.core.geometry.edit_object_placement(
|
||||
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
|
||||
)
|
||||
for similar_opening in similar_openings:
|
||||
similar_opening.ObjectPlacement = opening.ObjectPlacement
|
||||
building_objs.add(obj)
|
||||
for similar_opening in similar_openings:
|
||||
similar_opening.ObjectPlacement = opening.ObjectPlacement
|
||||
element = similar_opening.VoidsElements[0].RelatingBuildingElement
|
||||
obj = tool.Ifc.get_object(element)
|
||||
building_objs.add(obj)
|
||||
|
||||
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
|
||||
tool.Ifc.unlink(element=opening, obj=opening_obj)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
@@ -816,13 +832,10 @@ class CloneOpening(Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
|
||||
opening_placement = opening.ObjectPlacement
|
||||
opening_representations = opening.Representation.Representations
|
||||
opening_representation = opening.Representation
|
||||
|
||||
new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement")
|
||||
for representation in opening_representations:
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=new_opening, representation=representation
|
||||
)
|
||||
new_opening.Representation = opening_representation
|
||||
|
||||
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=new_opening, element=wall)
|
||||
new_opening.ObjectPlacement = opening_placement
|
||||
@@ -873,14 +886,14 @@ class DecorationsHandler:
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
verts = []
|
||||
selected_edges = []
|
||||
|
||||
@@ -87,8 +87,8 @@ class VIEW3D_MT_PIE_bim(bpy.types.Menu):
|
||||
pie.operator("bim.pie_add_opening")
|
||||
pie.operator("bim.pie_update_container")
|
||||
pie.operator("bim.open_pie_class", text="Assign IFC Class")
|
||||
pie.operator("bim.assign_object", text ="Assign Aggregation")
|
||||
pie.operator("bim.unassign_object", text ="Unassign Aggregation")
|
||||
pie.operator("bim.aggregate_assign_object", text="Assign Aggregation")
|
||||
pie.operator("bim.aggregate_unassign_object", text="Unassign Aggregation")
|
||||
|
||||
|
||||
class VIEW3D_MT_PIE_bim_class(bpy.types.Menu):
|
||||
|
||||
@@ -249,6 +249,8 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
mat.translation *= unit_scale
|
||||
mat = obj.matrix_world @ mat
|
||||
new_port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
|
||||
new_port.PredefinedType = port.PredefinedType
|
||||
new_port.SystemType = port.SystemType
|
||||
tool.Ifc.run("system.assign_port", element=element, port=new_port)
|
||||
tool.Ifc.run("geometry.edit_object_placement", product=new_port, matrix=mat, is_si=True)
|
||||
|
||||
|
||||
@@ -456,7 +456,9 @@ class DumbProfileJoiner:
|
||||
if not extrusion:
|
||||
return None, None
|
||||
position = extrusion.Position
|
||||
return (position.Axis.DirectionRatios, position.RefDirection.DirectionRatios)
|
||||
if position.Axis:
|
||||
return (position.Axis.DirectionRatios, position.RefDirection.DirectionRatios)
|
||||
return ((0.0, 0.0, 1.0), (1.0, 0.0, 0.0))
|
||||
|
||||
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
new_body = ifcopenshell.api.run(
|
||||
@@ -961,7 +963,7 @@ class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_axis(context))
|
||||
if not bpy.app.background:
|
||||
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ class BIMModelProperties(PropertyGroup):
|
||||
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=-pi / 180 * 89, max=pi / 180 * 89)
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
|
||||
# fmt: off
|
||||
type_template: bpy.props.EnumProperty(
|
||||
items=(
|
||||
("MESH", "Custom Mesh", "Use as a representation currently active object mesh or default cube if no object selected"),
|
||||
@@ -159,6 +160,7 @@ class BIMModelProperties(PropertyGroup):
|
||||
name="Type Template",
|
||||
default="MESH",
|
||||
)
|
||||
# fmt: on
|
||||
type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class)
|
||||
type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
@@ -191,6 +193,10 @@ class BIMArrayProperties(PropertyGroup):
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
def validate_nosing_value(self, context):
|
||||
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
||||
self["nosing_length"] = 0
|
||||
|
||||
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
|
||||
stair_types = (
|
||||
("CONCRETE", "Concrete", ""),
|
||||
@@ -201,13 +207,37 @@ class BIMStairProperties(PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01, subtype="DISTANCE")
|
||||
number_of_treads: bpy.props.IntProperty(name="Number of treads", default=6, soft_min=1)
|
||||
number_of_treads: bpy.props.IntProperty(name="Number of Treads", default=6, soft_min=1)
|
||||
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01, subtype="DISTANCE")
|
||||
tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE")
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base slab depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top slab depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has top nib", default=True)
|
||||
stair_type: bpy.props.EnumProperty(name="Stair type", items=stair_types, default="CONCRETE")
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
|
||||
stair_type: bpy.props.EnumProperty(
|
||||
name="Stair Type", items=stair_types, default="CONCRETE", update=validate_nosing_value
|
||||
)
|
||||
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
|
||||
name="Custom First / Last Treads Widths",
|
||||
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
|
||||
default=(0, 0),
|
||||
min=0,
|
||||
unit="LENGTH",
|
||||
size=2,
|
||||
)
|
||||
# TODO: need to clamp at zero for non WOOD/STEEL
|
||||
nosing_length: bpy.props.FloatProperty(
|
||||
name="Nosing Length",
|
||||
description=(
|
||||
"Overhang of the tread, not counted as a part of the tread run.\n"
|
||||
"Can be negative for WOOD/STEEL stair (then it becomes a tread gap)"
|
||||
),
|
||||
default=0,
|
||||
unit="LENGTH",
|
||||
update=validate_nosing_value,
|
||||
)
|
||||
nosing_depth: bpy.props.FloatProperty(
|
||||
name="Nosing Depth", description="Depth of the tread's nosing", min=0, default=0, unit="LENGTH"
|
||||
)
|
||||
|
||||
def get_props_kwargs(self, convert_to_project_units=False, stair_type=None):
|
||||
if not stair_type:
|
||||
@@ -218,10 +248,12 @@ class BIMStairProperties(PropertyGroup):
|
||||
"height": self.height,
|
||||
"number_of_treads": self.number_of_treads,
|
||||
"tread_run": self.tread_run,
|
||||
"nosing_length": self.nosing_length,
|
||||
}
|
||||
|
||||
if stair_type == "CONCRETE":
|
||||
concrete_props = {
|
||||
"nosing_depth": self.nosing_depth,
|
||||
"base_slab_depth": self.base_slab_depth,
|
||||
"top_slab_depth": self.top_slab_depth,
|
||||
"has_top_nib": self.has_top_nib,
|
||||
@@ -236,7 +268,13 @@ class BIMStairProperties(PropertyGroup):
|
||||
stair_kwargs.update(wood_steel_props)
|
||||
|
||||
elif stair_type == "GENERIC":
|
||||
pass
|
||||
generic_props = {
|
||||
"nosing_depth": self.nosing_depth,
|
||||
}
|
||||
stair_kwargs.update(generic_props)
|
||||
|
||||
# defined here to appear last in UI
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
|
||||
if not convert_to_project_units:
|
||||
return stair_kwargs
|
||||
|
||||
@@ -455,7 +455,7 @@ class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
if bpy.context.active_object.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: cancel_editing_railing_path(context))
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -537,7 +537,3 @@ class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_railing.bl_idname, icon="PLUGIN")
|
||||
|
||||
@@ -649,7 +649,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
if bpy.context.active_object.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
|
||||
def mark_preview_edges(bm, bew_verts, new_edges, new_faces):
|
||||
preview_layer = bm.edges.layers.int["BBIM_preview"]
|
||||
@@ -803,7 +803,3 @@ class SetGableRoofEdgeAngle(bpy.types.Operator):
|
||||
|
||||
tool.Blender.apply_bmesh(me, bm)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_roof.bl_idname, icon="PLUGIN")
|
||||
|
||||
@@ -288,7 +288,7 @@ class DumbSlabPlaner:
|
||||
"geometry.add_slab_representation",
|
||||
tool.Ifc.get(),
|
||||
context=body_context,
|
||||
depth=thickness,
|
||||
depth=thickness * self.unit_scale,
|
||||
x_angle=x_angle,
|
||||
)
|
||||
for inverse in tool.Ifc.get().get_inverse(representation):
|
||||
@@ -313,7 +313,7 @@ class DumbSlabPlaner:
|
||||
"geometry.add_slab_representation",
|
||||
tool.Ifc.get(),
|
||||
context=body_context,
|
||||
depth=thickness,
|
||||
depth=thickness * self.unit_scale,
|
||||
x_angle=x_angle,
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
@@ -625,7 +625,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context))
|
||||
if not bpy.app.background:
|
||||
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ from bmesh.types import BMVert
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
from ifcopenshell.util.shape_builder import V, ShapeBuilder
|
||||
|
||||
|
||||
def add_dumb_stair_object(self, context):
|
||||
@@ -78,129 +77,10 @@ class BIM_OT_add_object(Operator, AddObjectHelper):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def generate_stair_2d_profile(
|
||||
number_of_treads,
|
||||
height,
|
||||
width,
|
||||
tread_run,
|
||||
stair_type,
|
||||
# WOOD/STEEL CONCRETE STAIR ARGUMENTS
|
||||
tread_depth=None,
|
||||
# CONCRETE STAIR ARGUMENTS
|
||||
has_top_nib=None,
|
||||
top_slab_depth=None,
|
||||
base_slab_depth=None,
|
||||
):
|
||||
vertices = []
|
||||
edges = []
|
||||
faces = []
|
||||
|
||||
number_of_risers = number_of_treads + 1
|
||||
tread_rise = height / number_of_risers
|
||||
length = tread_run * number_of_risers
|
||||
|
||||
if stair_type == "WOOD/STEEL":
|
||||
builder = ShapeBuilder(None)
|
||||
tread_shape = builder.get_rectangle_coords(
|
||||
size=V(tread_run, 0, tread_depth), position=V(0, 0, -(tread_depth - tread_rise))
|
||||
)
|
||||
tread_offset = V(tread_run, 0, tread_rise)
|
||||
|
||||
for i in range(number_of_risers):
|
||||
cur_trade_shape = [v + tread_offset * i for v in tread_shape]
|
||||
vertices.extend(cur_trade_shape)
|
||||
|
||||
cur_vertex = i * 4
|
||||
edges.extend(
|
||||
[
|
||||
(cur_vertex, cur_vertex + 1),
|
||||
(cur_vertex + 1, cur_vertex + 2),
|
||||
(cur_vertex + 2, cur_vertex + 3),
|
||||
(cur_vertex + 3, cur_vertex),
|
||||
]
|
||||
)
|
||||
faces.append(list(range(cur_vertex, cur_vertex + 1)))
|
||||
|
||||
return (vertices, edges, faces)
|
||||
|
||||
elif stair_type == "GENERIC":
|
||||
vertices.append(Vector([0, 0, 0]))
|
||||
|
||||
tread_verts = [Vector([0, 0, tread_rise]), Vector([tread_run, 0, tread_rise])]
|
||||
tread_offset = Vector([tread_run, 0, tread_rise])
|
||||
|
||||
for i in range(number_of_risers):
|
||||
current_tread_verts = [v + tread_offset * i for v in tread_verts]
|
||||
last_vert_i = len(vertices) - 1
|
||||
edges.extend([(last_vert_i, last_vert_i + 1), (last_vert_i + 1, last_vert_i + 2)])
|
||||
vertices.extend(current_tread_verts)
|
||||
|
||||
last_vert_i = len(vertices)
|
||||
vertices.append(vertices[-1] * V(1, 0, 0))
|
||||
edges.extend([(last_vert_i - 1, last_vert_i), (last_vert_i, 0)])
|
||||
|
||||
return (vertices, edges, faces)
|
||||
|
||||
elif stair_type == "CONCRETE":
|
||||
for i in range(number_of_risers):
|
||||
vertices.extend(
|
||||
[Vector((tread_run * i, 0, tread_rise * i)), Vector((tread_run * i, 0, tread_rise * (i + 1)))]
|
||||
)
|
||||
cur_vertex = i * 2
|
||||
if i != 0:
|
||||
edges.append((cur_vertex - 1, cur_vertex))
|
||||
edges.append((cur_vertex, cur_vertex + 1))
|
||||
|
||||
vertices.append(Vector((tread_run * number_of_risers, 0, tread_rise * number_of_risers)))
|
||||
edges.append((number_of_risers * 2, number_of_risers * 2 - 1))
|
||||
|
||||
td_vector = Vector((vertices[2][2], 0, -vertices[2][0])).normalized() * tread_depth
|
||||
|
||||
k = tread_rise / tread_run
|
||||
s0 = vertices[0] + td_vector
|
||||
b = s0.z - k * s0.x # comes from y = kx + b
|
||||
# you could use td_vector as depth_vector
|
||||
# but then stair won't be perpendicular to the X+
|
||||
# b is kind of vertical tread_depth (along Z+)
|
||||
depth_vector = Vector((0, 0, b))
|
||||
|
||||
# top nib
|
||||
if has_top_nib:
|
||||
vertices.append(vertices[number_of_risers * 2] + Vector((0, 0, -top_slab_depth)))
|
||||
vertices.append(vertices[number_of_risers * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth)))
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append((number_of_risers * 2, last_vertex_i - 1))
|
||||
edges.append((last_vertex_i - 1, last_vertex_i))
|
||||
else:
|
||||
vertices.append(vertices[number_of_risers * 2] + depth_vector)
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append((number_of_risers * 2, last_vertex_i))
|
||||
|
||||
top_nib_end = len(vertices) - 1
|
||||
|
||||
# bottom nib
|
||||
if abs(b) <= base_slab_depth:
|
||||
vertices.append(vertices[0] + depth_vector)
|
||||
edges.append((0, len(vertices) - 1))
|
||||
bottom_nib_end = len(vertices) - 1
|
||||
else:
|
||||
vertices.append(vertices[0] + Vector(((-base_slab_depth - b) / k, 0, -base_slab_depth)))
|
||||
vertices.append(vertices[0] + Vector((0, 0, -base_slab_depth)))
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append((0, last_vertex_i))
|
||||
edges.append((last_vertex_i - 1, last_vertex_i))
|
||||
bottom_nib_end = len(vertices) - 2
|
||||
|
||||
edges.append((bottom_nib_end, top_nib_end))
|
||||
faces = [list(range(len(vertices)))]
|
||||
|
||||
return (vertices, edges, faces)
|
||||
|
||||
|
||||
def regenerate_stair_mesh(context):
|
||||
obj = context.active_object
|
||||
props_kwargs = obj.BIMStairProperties.get_props_kwargs()
|
||||
vertices, edges, faces = generate_stair_2d_profile(**props_kwargs)
|
||||
vertices, edges, faces = tool.Model.generate_stair_2d_profile(**props_kwargs)
|
||||
|
||||
obj = context.active_object
|
||||
bm = bmesh.new()
|
||||
@@ -266,6 +146,7 @@ def update_ifc_stair_props(obj):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
riser_height = props.height / number_of_risers / si_conversion
|
||||
tread_length = props.tread_depth / si_conversion
|
||||
nosing_length = props.nosing_length / si_conversion
|
||||
|
||||
if element.is_a("IfcStairFlight"):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
@@ -291,6 +172,7 @@ def update_ifc_stair_props(obj):
|
||||
"NumberOfTreads": props.number_of_treads,
|
||||
"RiserHeight": riser_height,
|
||||
"TreadLength": tread_length,
|
||||
"NosingLength": nosing_length,
|
||||
},
|
||||
)
|
||||
tool.Ifc.edit(obj)
|
||||
@@ -458,8 +340,3 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN")
|
||||
self.layout.operator(BIM_OT_add_clever_stair.bl_idname, icon="PLUGIN")
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel, Menu
|
||||
from blenderbim.bim.module.model.data import (
|
||||
@@ -36,6 +37,7 @@ from blenderbim.bim.module.model.door import update_door_modifier_bmesh
|
||||
from blenderbim.bim.module.model.railing import update_railing_modifier_bmesh
|
||||
from blenderbim.bim.module.model.roof import update_roof_modifier_bmesh
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
class LaunchTypeManager(bpy.types.Operator):
|
||||
@@ -259,35 +261,42 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Stair parameters", icon="IPO_CONSTANT")
|
||||
|
||||
stair_data = StairData.data["pset_data"]["data_dict"]
|
||||
if props.is_editing:
|
||||
calculated_params = tool.Model.get_active_stair_calculated_params()
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_name in props.get_props_kwargs():
|
||||
self.layout.prop(props, prop_name)
|
||||
prop_value = getattr(props, prop_name)
|
||||
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||
self.layout.label(text=f"{prop_readable_name}:")
|
||||
self.layout.prop(props, prop_name, text="")
|
||||
else:
|
||||
self.layout.prop(props, prop_name)
|
||||
regenerate_stair_mesh(context)
|
||||
else:
|
||||
calculated_params = StairData.data["calculated_params"]
|
||||
row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_stair", icon="X", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_name, prop_value in StairData.data["general_params"].items():
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=prop_name)
|
||||
row.label(text=str(prop_value))
|
||||
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||
row.label(text=f"{prop_name}:")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_value_item in prop_value:
|
||||
row.label(text=str(prop_value_item))
|
||||
else:
|
||||
row.label(text=prop_name)
|
||||
row.label(text=str(prop_value))
|
||||
|
||||
# calculated properties
|
||||
number_of_rises = props.number_of_treads + 1
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Number of risers")
|
||||
row.label(text=str(number_of_rises))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Tread rise")
|
||||
row.label(text=str(round(props.height / number_of_rises, 5)))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Length")
|
||||
row.label(text=str(round(props.tread_run * number_of_rises, 5)))
|
||||
for prop_name, prop_value in calculated_params.items():
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=prop_name)
|
||||
row.label(text=str(prop_value))
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Stair Found")
|
||||
@@ -644,3 +653,14 @@ class BIM_MT_model(Menu):
|
||||
|
||||
def add_menu(self, context):
|
||||
self.layout.menu("BIM_MT_model", icon="FILE_3D")
|
||||
|
||||
|
||||
def add_mesh_object_menu(self, context):
|
||||
self.layout.separator()
|
||||
self.layout.operator("mesh.add_grid", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("mesh.add_stair", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("mesh.add_clever_stair", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("mesh.add_window", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("mesh.add_door", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("mesh.add_railing", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("mesh.add_roof", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
|
||||
@@ -593,7 +593,3 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_window.bl_idname, icon="PLUGIN")
|
||||
|
||||
@@ -26,6 +26,7 @@ from blenderbim.bim.helper import prop_with_search, close_operator_panel
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.drawing.data import DecoratorData
|
||||
from blenderbim.bim.module.system.data import PortData
|
||||
from blenderbim.bim.module.model.prop import get_ifc_class
|
||||
|
||||
|
||||
@@ -193,6 +194,7 @@ class PipeTool(BimTool):
|
||||
def add_layout_hotkey_operator(layout, text, hotkey, description):
|
||||
modifiers = {
|
||||
"A": "EVENT_ALT",
|
||||
"C": "EVENT_CTRL",
|
||||
"S": "EVENT_SHIFT",
|
||||
}
|
||||
modifier, key = hotkey.split("_")
|
||||
@@ -218,6 +220,9 @@ class BimToolUI:
|
||||
row.label(text="No IFC Project", icon="ERROR")
|
||||
return
|
||||
|
||||
if not PortData.is_loaded:
|
||||
PortData.load()
|
||||
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load(ifc_element_type)
|
||||
elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None:
|
||||
@@ -348,14 +353,12 @@ class BimToolUI:
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
|
||||
)
|
||||
|
||||
if context.region.type != "TOOL_HEADER":
|
||||
cls.layout.operator("bim.mep_add_bend")
|
||||
cls.layout.operator("bim.mep_add_transition")
|
||||
cls.layout.operator("bim.mep_add_obstruction")
|
||||
cls.layout.operator("bim.mep_connect_elements")
|
||||
|
||||
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__)
|
||||
@@ -402,11 +405,17 @@ class BimToolUI:
|
||||
row.label(text="", icon=f"EVENT_TAB")
|
||||
row.operator("bim.enable_editing_roof_path", text="Edit Roof Path")
|
||||
|
||||
if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0:
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
|
||||
)
|
||||
cls.layout.operator("bim.mep_connect_elements")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
|
||||
if len(context.selected_objects) == 2:
|
||||
if len(context.selected_objects) > 1:
|
||||
row.operator("bim.add_opening", text="Apply Void")
|
||||
else:
|
||||
row.operator("bim.add_potential_opening", text="Add Void")
|
||||
@@ -419,6 +428,8 @@ class BimToolUI:
|
||||
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
|
||||
|
||||
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
|
||||
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
|
||||
row.operator("bim.hide_openings", icon="CANCEL", text="")
|
||||
if len(context.selected_objects) == 2:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -435,6 +446,10 @@ class BimToolUI:
|
||||
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
|
||||
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
|
||||
|
||||
cls.layout.row(align=True).label(text="Aggregation")
|
||||
add_layout_hotkey_operator(cls.layout, "Assign", "C_P", bpy.ops.bim.aggregate_assign_object.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Unassign", "A_P", bpy.ops.bim.aggregate_unassign_object.__doc__)
|
||||
|
||||
cls.layout.separator()
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Calculate All Quantities", "S_Q", bpy.ops.bim.calculate_all_quantities.__doc__
|
||||
@@ -570,12 +585,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def hotkey_C_P(self):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
bpy.ops.bim.assign_object()
|
||||
bpy.ops.bim.aggregate_assign_object()
|
||||
|
||||
def hotkey_A_P(self):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
bpy.ops.bim.unassign_object()
|
||||
bpy.ops.bim.aggregate_unassign_object()
|
||||
|
||||
def hotkey_S_C(self):
|
||||
if not bpy.context.selected_objects:
|
||||
@@ -676,21 +691,23 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.fit_flow_segments()
|
||||
|
||||
def hotkey_S_G(self):
|
||||
obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not bpy.context.selected_objects:
|
||||
if self.props.ifc_class == "IfcSpaceType":
|
||||
bpy.ops.bim.generate_space()
|
||||
return
|
||||
if self.active_material_usage == "LAYER2":
|
||||
bpy.ops.bim.recalculate_wall()
|
||||
elif tool.System.get_ports(element):
|
||||
bpy.ops.bim.regenerate_distribution_element()
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
if self.active_class in (
|
||||
if self.active_class not in (
|
||||
"IfcCableCarrierSegment",
|
||||
"IfcCableSegment",
|
||||
"IfcDuctSegment",
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
bpy.ops.bim.regenerate_distribution_element()
|
||||
else:
|
||||
bpy.ops.bim.recalculate_profile()
|
||||
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
|
||||
bpy.ops.bim.recalculate_fill()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.BIM_OT_nest_assign_object,
|
||||
operator.BIM_OT_disable_editing_nest,
|
||||
operator.BIM_OT_enable_editing_nest,
|
||||
operator.BIM_OT_select_components,
|
||||
operator.BIM_OT_select_nest,
|
||||
operator.BIM_OT_nest_unassign_object,
|
||||
prop.BIMObjectNestProperties,
|
||||
ui.BIM_PT_nest,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Object.BIMObjectNestProperties = bpy.props.PointerProperty(type=prop.BIMObjectNestProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Object.BIMObjectNestProperties
|
||||
@@ -0,0 +1,67 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def refresh():
|
||||
NestData.is_loaded = False
|
||||
|
||||
|
||||
class NestData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"has_relating_object": cls.has_relating_object(),
|
||||
"relating_object_label": cls.get_relating_object_label(),
|
||||
"has_related_objects": cls.has_related_objects(),
|
||||
"total_components": cls.total_components(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def get_relating_object(cls):
|
||||
return ifcopenshell.util.element.get_nest(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
|
||||
@classmethod
|
||||
def has_relating_object(cls) -> bool:
|
||||
return cls.get_relating_object() is not None
|
||||
|
||||
@classmethod
|
||||
def get_relating_object_label(cls) -> str:
|
||||
nest = cls.get_relating_object()
|
||||
if nest:
|
||||
return f"{nest.is_a()}/{nest.Name or ''}"
|
||||
|
||||
@classmethod
|
||||
def get_related_objects(cls):
|
||||
return ifcopenshell.util.element.get_components(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
|
||||
@classmethod
|
||||
def total_components(cls):
|
||||
components = cls.get_related_objects()
|
||||
return len(components) if components else 0
|
||||
|
||||
@classmethod
|
||||
def has_related_objects(cls) -> bool:
|
||||
return bool(cls.get_related_objects())
|
||||
@@ -0,0 +1,151 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.nest as core
|
||||
import blenderbim.core.spatial
|
||||
import blenderbim.bim.handler
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class Operator:
|
||||
def execute(self, context):
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_nest_assign_object(bpy.types.Operator, Operator):
|
||||
"""Create nest relationship between two ifc elements"""
|
||||
|
||||
bl_idname = "bim.nest_assign_object"
|
||||
bl_label = "Assign Object To Nesting"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
relating_object: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
relating_obj = None
|
||||
if self.relating_object:
|
||||
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
|
||||
elif context.active_object:
|
||||
relating_obj = context.active_object
|
||||
if not relating_obj:
|
||||
return
|
||||
|
||||
for obj in bpy.context.selected_objects:
|
||||
if obj == relating_obj:
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
result = core.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Nest,
|
||||
tool.Collector,
|
||||
relating_obj=relating_obj,
|
||||
related_obj=obj,
|
||||
)
|
||||
if not result:
|
||||
self.report({"ERROR"}, f" Cannot nest {obj.name} to {relating_obj.name}")
|
||||
|
||||
|
||||
class BIM_OT_nest_unassign_object(bpy.types.Operator, Operator):
|
||||
"""Remove nest relationship between two ifc elements"""
|
||||
|
||||
bl_idname = "bim.nest_unassign_object"
|
||||
bl_label = "Unassign Object From Nesting"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
if not nest:
|
||||
continue
|
||||
core.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Nest,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(nest),
|
||||
related_obj=tool.Ifc.get_object(element),
|
||||
)
|
||||
|
||||
|
||||
class BIM_OT_enable_editing_nest(bpy.types.Operator, Operator):
|
||||
"""Enable editing nest relationship"""
|
||||
|
||||
bl_idname = "bim.enable_editing_nest"
|
||||
bl_label = "Enable Editing Nest"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.enable_editing_nest(tool.Nest, obj=context.active_object)
|
||||
|
||||
|
||||
class BIM_OT_disable_editing_nest(bpy.types.Operator, Operator):
|
||||
"""Disable editing nest relationship"""
|
||||
|
||||
bl_idname = "bim.disable_editing_nest"
|
||||
bl_label = "Disable Editing Nest"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.disable_editing_nest(tool.Nest, obj=context.active_object)
|
||||
|
||||
|
||||
class BIM_OT_select_components(bpy.types.Operator):
|
||||
"""Select Components"""
|
||||
|
||||
bl_idname = "bim.select_components"
|
||||
bl_label = "Select Components"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(obj))
|
||||
component_objs = set(tool.Ifc.get_object(c) for c in components)
|
||||
selectable_component_objs = set(context.selectable_objects).intersection(component_objs)
|
||||
for selectable_component_obj in selectable_component_objs:
|
||||
selectable_component_obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_select_nest(bpy.types.Operator):
|
||||
"""Select Nest"""
|
||||
|
||||
bl_idname = "bim.select_nest"
|
||||
bl_label = "Select Nest"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
nest = ifcopenshell.util.element.get_nest(tool.Ifc.get_entity(obj))
|
||||
nest_obj = tool.Ifc.get_object(nest)
|
||||
if nest_obj in context.selectable_objects:
|
||||
nest_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = nest_obj
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,48 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.module.spatial.data import SpatialData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
def update_relating_object(self, context):
|
||||
def message(self, context):
|
||||
self.layout.label(text="Please select a valid Ifc Element")
|
||||
|
||||
if self.relating_object is None:
|
||||
return
|
||||
if not self.relating_object.BIMObjectProperties.ifc_definition_id:
|
||||
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
|
||||
self.relating_object = None
|
||||
|
||||
|
||||
class BIMObjectNestProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_object: PointerProperty(name="Nest Host", type=bpy.types.Object, update=update_relating_object)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user