mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-08 00:41:36 +00:00
Compare commits
318 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8f18b5b6d | |||
| 718254fe5e | |||
| 41227b89c8 | |||
| 2547ee9ab5 | |||
| d22f1bcd71 | |||
| 0c2158089d | |||
| ccafc203b5 | |||
| 0efc2d25f1 | |||
| ac50f002f0 | |||
| 66f4c957ea | |||
| 67016d6f90 | |||
| 439f9c14ce | |||
| 404bf13a18 | |||
| 3a37bcdcaa | |||
| 992ad8e24a | |||
| 1de2515fcd | |||
| 4bc88eac2f | |||
| 22f10e5549 | |||
| 8e34c1d7df | |||
| 4369baa226 | |||
| 7941d71feb | |||
| 68d967c2e5 | |||
| 4ea9a598c7 | |||
| 06d1c81954 | |||
| aa5f08880b | |||
| 9e6320d545 | |||
| f39d3d4b9c | |||
| 5c72cf35bd | |||
| b9a435e1c3 | |||
| 92f2559ce0 | |||
| acf9713943 | |||
| a77125ff12 | |||
| c8841bcb6a | |||
| b20997dea8 | |||
| 292aca6685 | |||
| 3a824ec969 | |||
| bea9f42f87 | |||
| e4a50b32ce | |||
| 76611024b6 | |||
| 92ee82374c | |||
| af81aa1fae | |||
| 16f9f91561 | |||
| 3a52a848ae | |||
| a50c633cf4 | |||
| 6251750133 | |||
| acef6379e8 | |||
| c2860e6e25 | |||
| b8e5cd740a | |||
| 9188dc60ab | |||
| 8ce56db0c3 | |||
| b91b1401f4 | |||
| 0e37528915 | |||
| 9991c4aa56 | |||
| 0ef3983035 | |||
| 0f6e6a48f4 | |||
| 3d225beafd | |||
| 6a91bbb720 | |||
| 1f23e4ba18 | |||
| 2ebcffc7d4 | |||
| 3d4321f713 | |||
| f8799305e1 | |||
| 7c38705958 | |||
| f6fc0cfa0f | |||
| 686e9c946b | |||
| 3085072751 | |||
| 0328be4093 | |||
| 6218d3902e | |||
| be1836fa48 | |||
| 22b5db8a1e | |||
| 4268e1376c | |||
| e2c1a54e0a | |||
| d1be660469 | |||
| 3ae5227b57 | |||
| cdc994ca76 | |||
| 0ad4087f0a | |||
| 4b7ca648c6 | |||
| 17bd6e6b0c | |||
| 7b80803cbe | |||
| eaf67d83f9 | |||
| 41f754e15b | |||
| b7cdb86327 | |||
| 809a20dc99 | |||
| f5e41d18c6 | |||
| 60a8c45ad7 | |||
| 73f98bfe4b | |||
| 958686958b | |||
| e0b61c5794 | |||
| 5aa5552759 | |||
| 82a19cee7b | |||
| 43d4adbd9d | |||
| 1d3c71985f | |||
| 381fca0812 | |||
| 0f37eca333 | |||
| 6bf9b8b0c9 | |||
| 9361cfee28 | |||
| 1c205d494a | |||
| 4d1d224a34 | |||
| 4976af5e66 | |||
| be657748f3 | |||
| def9352ade | |||
| a20002ecca | |||
| b7a0f230ad | |||
| 76529a06bb | |||
| dcdae67a2d | |||
| 5a7a368169 | |||
| ec13294430 | |||
| 653b88ba18 | |||
| f8489779f6 | |||
| 64df82a76b | |||
| af4fbd6750 | |||
| 1619ae8543 | |||
| 41eadf735e | |||
| 172775d4ed | |||
| d539cad1c2 | |||
| 94cf56e080 | |||
| cf253824ca | |||
| 9c73f17303 | |||
| a6f523039b | |||
| c2f9bfeb8d | |||
| 3ab1c2e0f9 | |||
| 0ed964daa2 | |||
| 03f619a77c | |||
| e5ad35e2c8 | |||
| 297f15737d | |||
| 374b95efdf | |||
| 744ed522b6 | |||
| f4bc3fffa3 | |||
| 06a2cecb51 | |||
| b0f2785e99 | |||
| c6c852c7f6 | |||
| 5d920d5e31 | |||
| 2b522b71cb | |||
| d00c3735ec | |||
| bbdc9c45dd | |||
| 5eb9cdf40d | |||
| 689289bb60 | |||
| d401e4c948 | |||
| cbe8fc1ebd | |||
| 9f11568aff | |||
| 968e193f99 | |||
| 2d06a79842 | |||
| 808a16ba4a | |||
| a67a6034a5 | |||
| 8fe1a63339 | |||
| 1ce78f3c65 | |||
| 57b35c68e4 | |||
| ff57b91691 | |||
| 6e15219fad | |||
| 979964f211 | |||
| 4ce2f81479 | |||
| dbdde52435 | |||
| 84568f34f0 | |||
| fe9c548ae0 | |||
| 1714516a81 | |||
| c4c3a2056b | |||
| fad35b14f2 | |||
| 2382f8d7f8 | |||
| 9ae7a68c43 | |||
| 7a5e5fe602 | |||
| 839b2c9c1f | |||
| 4aaaf6e8f5 | |||
| 1b38b03532 | |||
| cbf0fa88eb | |||
| 6961ca8ce7 | |||
| d758d4aef5 | |||
| 84240c870c | |||
| 4483d8e958 | |||
| 70440050db | |||
| bc7527a73b | |||
| c8ad1ba0ee | |||
| c7a29d5604 | |||
| 814c16d3ee | |||
| 3bedbfb82c | |||
| 07f42b9126 | |||
| 5b1da583d9 | |||
| c624162f09 | |||
| bc221cfa99 | |||
| 5d02afa47e | |||
| 406962287c | |||
| 241f7679b0 | |||
| 0001071185 | |||
| 8d476c792d | |||
| 97c7aefad7 | |||
| 34cafc761a | |||
| 3fbde8db28 | |||
| 0b8fb41ac4 | |||
| dc46afc227 | |||
| b8ee1d4d8d | |||
| ecf461e1e0 | |||
| 1856eeddeb | |||
| afb2b9fdea | |||
| af2cb2896a | |||
| e792cf8fa0 | |||
| 7b71e2ce2a | |||
| ce72337d3a | |||
| 0e9ca541c4 | |||
| 784576b249 | |||
| 5e26e55a8c | |||
| 1b110c4a84 | |||
| b94629133c | |||
| 55e270c135 | |||
| 308d628b1e | |||
| 5e16588eb7 | |||
| e919a0643f | |||
| 9835ef977f | |||
| 10f84537de | |||
| f9a0081b33 | |||
| 11462c388a | |||
| 4bcd9639f4 | |||
| b856c40fe7 | |||
| dd1c529544 | |||
| c9fbec4d46 | |||
| c9437329ef | |||
| a09e971674 | |||
| 8ac04e32e5 | |||
| 0634dc83d4 | |||
| 5dc4b18d3f | |||
| f84bca8017 | |||
| 31a6c91c4b | |||
| 30242d859e | |||
| 330799c799 | |||
| e2c2234199 | |||
| 34efa9f459 | |||
| 261de9b2be | |||
| 52af302bef | |||
| 529c74f6a9 | |||
| fd30a6b5eb | |||
| 82f25f50cc | |||
| d0dc31588d | |||
| 68f71c715e | |||
| 6913060f0c | |||
| 4427c73816 | |||
| d188155fac | |||
| 598dfd75e7 | |||
| 5c63cd7981 | |||
| 0b22333b69 | |||
| 69481c2d63 | |||
| 671c887201 | |||
| 0a6fbeccb0 | |||
| 4b3ca11c12 | |||
| f9bf8897bc | |||
| 932939099a | |||
| 8d31d93531 | |||
| 5c4f157624 | |||
| f87c694432 | |||
| 38eedffd80 | |||
| 305439d27e | |||
| 908bf02098 | |||
| c6176d364f | |||
| 068814c095 | |||
| 476604202a | |||
| 558bda1aea | |||
| 426f1b91f2 | |||
| d1b1b08174 | |||
| c1513190d4 | |||
| 50e7728f95 | |||
| 4e00a8d596 | |||
| 266cc32336 | |||
| f8ad05cb54 | |||
| 311f9b96e7 | |||
| 56bfe80b3b | |||
| 2db6b860b2 | |||
| 5af2c3914a | |||
| f8ebc2d156 | |||
| f3228c31f8 | |||
| 86d481f4f8 | |||
| 05f79c844a | |||
| b87a7b3d0a | |||
| e54acd83f8 | |||
| f8e52c8a12 | |||
| 0608f92bc4 | |||
| df18108406 | |||
| b64dcfd4b8 | |||
| d1493b4234 | |||
| fb34af9162 | |||
| bb598d848a | |||
| 502cf6f439 | |||
| 9875fce579 | |||
| 90198115b6 | |||
| 4896a69981 | |||
| 3390f58c88 | |||
| 917d28a7b1 | |||
| bebaa4d1bd | |||
| 93a4f4980a | |||
| 1e30eae1d7 | |||
| ff90396bcf | |||
| a030b84fc5 | |||
| 0b1c049e81 | |||
| 74390c0529 | |||
| 21d39d8a23 | |||
| ad0f62f88f | |||
| 85d8fa49ab | |||
| c5f4b53555 | |||
| 016cfa94d2 | |||
| 007af80234 | |||
| 576a206d5b | |||
| 4cf6411a61 | |||
| 37a0b3f6a9 | |||
| 209caec49a | |||
| bebb29f486 | |||
| deb6e3779e | |||
| a6c37771fc | |||
| 95f754d2df | |||
| 030dc6460a | |||
| 360e08f268 | |||
| dc84d85f7b | |||
| e16d8907f3 | |||
| 52cada180b | |||
| 1999216ea5 | |||
| 5a6476a57f | |||
| c45e3b0099 | |||
| c9fc543e91 | |||
| 9994fec571 | |||
| b0aeabf2f6 | |||
| ddd7dff75e | |||
| 7acd13cb82 | |||
| 6499ac41cf | |||
| e8b74245ce |
@@ -118,5 +118,10 @@ jobs:
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Upload .zip Archives to S3
|
||||
env:
|
||||
AWS_DEBUG: 1
|
||||
AWS_RETRY_MODE: standard
|
||||
AWS_MAX_ATTEMPTS: 3
|
||||
run: |
|
||||
aws s3 cp $env:USERPROFILE\output s3://ifcopenshell-builds/ --recursive
|
||||
dir "$env:USERPROFILE\output"
|
||||
aws s3 cp "$env:USERPROFILE\output" s3://ifcopenshell-builds/ --recursive --debug
|
||||
|
||||
@@ -19,55 +19,34 @@ jobs:
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Step 1 - install dependencies
|
||||
shell: bash
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install 'black>=24.10.0'
|
||||
|
||||
# NOTE: This would suffice, however it is less informative in terms of the 3 possible outcomes
|
||||
# - name: QA Step - check linting
|
||||
# shell: bash
|
||||
# id: linting
|
||||
# run: |
|
||||
# python3 -m black .
|
||||
|
||||
# QA STEP
|
||||
- name: QA Step - check linting
|
||||
shell: bash
|
||||
id: linting
|
||||
# black doesn't catch all syntax errors, so we check them explicitly.
|
||||
- name: Check syntax errors
|
||||
id: syntax-errors
|
||||
run: |
|
||||
python3 -m black --check . \
|
||||
&& exit 0 \
|
||||
|| (echo "exit_code=$?" >> "$GITHUB_OUTPUT" && exit 1);
|
||||
ERROR=0
|
||||
python3 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
||||
python3 -W error -m compileall -q src/bonsai || ERROR=1
|
||||
exit $ERROR
|
||||
continue-on-error: true
|
||||
|
||||
# OUTCOME 1 of QA STEP
|
||||
- name: QA Step - no linting errors
|
||||
if: steps.linting.outcome == 'success'
|
||||
shell: bash
|
||||
run: |-
|
||||
echo "::notice::QA step linting succeeded"
|
||||
exit 0;
|
||||
- name: Black formatter
|
||||
id: linting
|
||||
run: |
|
||||
python3 -m black --diff --check .
|
||||
continue-on-error: true
|
||||
|
||||
# OUTCOME 2i of QA STEP
|
||||
- name: QA Step - unprettified code with no syntax errors
|
||||
if: steps.linting.outputs.exit_code == 1
|
||||
shell: bash
|
||||
run: |-
|
||||
echo "::group::QA step succeeded with warnings"
|
||||
echo "::warning::one or more files contains unformatted code but no syntax errors";
|
||||
echo "::notice::please run the linter before pushing!";
|
||||
echo "::endgroup::"
|
||||
exit 0;
|
||||
|
||||
# OUTCOME 2ii of QA STEP
|
||||
- name: QA Step - code contains syntax errors
|
||||
if: steps.linting.outputs.exit_code == 123
|
||||
shell: bash
|
||||
run: |-
|
||||
echo "::group::QA step failed"
|
||||
echo "::error::one or more files contains syntax errors";
|
||||
echo "::notice::please run the linter and fix syntax errors before pushing!";
|
||||
echo "::endgroup::"
|
||||
exit 1;
|
||||
- name: Final check
|
||||
run: |
|
||||
ERROR=0
|
||||
if [ "${{ steps.syntax-errors.outcome }}" != "success" ]; then
|
||||
echo "::error::Syntax errors check failed, see 'syntax-errors' step for the details." && ERROR=1
|
||||
fi
|
||||
if [ "${{ steps.linting.outcome }}" != "success" ]; then
|
||||
echo "::error::Black formatting check failed, see 'linting' step for the details." && ERROR=1
|
||||
fi
|
||||
exit $ERROR
|
||||
|
||||
@@ -55,6 +55,8 @@ jobs:
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
@@ -175,6 +177,7 @@ jobs:
|
||||
|
||||
cd IfcOpenShell/src/bonsai
|
||||
pip install pytest-blender
|
||||
pip install pytest-bdd
|
||||
blender --background --python scripts/setup_pytest.py
|
||||
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
|
||||
make test
|
||||
|
||||
@@ -35,11 +35,11 @@ jobs:
|
||||
|
||||
- name: Version + date str
|
||||
id: verdate
|
||||
run: echo "verdate='${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}'" >> $GITHUB_OUTPUT
|
||||
run: echo "verdate=${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
test:
|
||||
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}-${{ matrix.variant }}
|
||||
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
|
||||
needs: activate
|
||||
runs-on: ${{ matrix.platform.distver }}
|
||||
defaults:
|
||||
@@ -48,57 +48,67 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: [novtk, all]
|
||||
pyver: [
|
||||
# { name: py311, distver: '3.11'},
|
||||
{ name: py312, distver: '3.12'}
|
||||
]
|
||||
platform: [
|
||||
{ name: win, distver: windows-latest, pkg_dir: 'win-64' },
|
||||
{ name: linux, distver: ubuntu-latest, pkg_dir: 'linux-64' },
|
||||
{ name: macOS, distver: macos-latest, pkg_dir: 'osx-64' }
|
||||
{ name: macOS-arm, distver: macos-latest, pkg_dir: 'osx-arm64' },
|
||||
{ name: macOS-x86, distver: macos-13, pkg_dir: 'osx-64' }
|
||||
]
|
||||
steps:
|
||||
- name: Set up swap space
|
||||
- name: Set Swap Space
|
||||
if: runner.os == 'Linux'
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
|
||||
- name: set ARTIFACTS ENV vars
|
||||
shell: bash
|
||||
run: |
|
||||
pwd
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
echo "ARTIFACTS_DIR=D:/a/artifacts" >> $GITHUB_ENV
|
||||
elif [[ "$RUNNER_OS" == "macOS" ]]; then
|
||||
echo "ARTIFACTS_DIR=/Users/runner/work/artifacts" >> $GITHUB_ENV
|
||||
elif [[ "$RUNNER_OS" == "Linux" ]]; then
|
||||
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download and extract MacOSX SDK
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
if: ${{ matrix.platform.name == 'macOS-x86' }}
|
||||
run: |
|
||||
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
|
||||
|
||||
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
environment-name: test-env
|
||||
create-args: >-
|
||||
python=3.11
|
||||
python=3.12
|
||||
anaconda-client
|
||||
boa
|
||||
rattler-build
|
||||
|
||||
- name: create conda package dist dir
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/dist
|
||||
mkdir -p ${{ env.ARTIFACTS_DIR }}
|
||||
|
||||
- name: build & test ifcopenshell
|
||||
run: |
|
||||
conda mambabuild . --python ${{ matrix.pyver.distver }} --no-remove-work-dir --output-folder '${{ github.workspace }}/dist' --variants='{variant: ${{ matrix.variant }}}'
|
||||
working-directory: ./conda
|
||||
rattler-build build -r conda/recipe.yaml --output-dir '${{ env.ARTIFACTS_DIR }}'
|
||||
env:
|
||||
VERSION_OVERRIDE: ${{ needs.activate.outputs.verdate }}
|
||||
|
||||
- name: upload to anaconda
|
||||
if: ${{ matrix.platform.name == 'win' }}
|
||||
run: |
|
||||
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2'
|
||||
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda'
|
||||
|
||||
- name: upload to anaconda
|
||||
if: ${{ matrix.platform.name != 'win' }}
|
||||
run: |
|
||||
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2
|
||||
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda
|
||||
|
||||
@@ -49,3 +49,7 @@ if errorlevel 1 exit 1
|
||||
ninja install -j 1
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
|
||||
python %RECIPE_DIR%/update_version_init.py %PKG_VERSION% %SP_DIR%/ifcopenshell/__init__.py
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
+3
-1
@@ -46,4 +46,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
|
||||
|
||||
ninja
|
||||
|
||||
ninja install -j 1
|
||||
ninja install -j 1
|
||||
|
||||
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
|
||||
@@ -1,7 +1,3 @@
|
||||
variant:
|
||||
- novtk
|
||||
- all
|
||||
|
||||
occt:
|
||||
- 7.8.1
|
||||
|
||||
@@ -24,12 +20,13 @@ cxx_compiler_version:
|
||||
- '12' # [linux]
|
||||
- '16' # [osx]
|
||||
c_stdlib_version:
|
||||
- '2.12' # [linux]
|
||||
- '10.13' # [osx]
|
||||
- 2.17 # [linux]
|
||||
- 10.13 # [osx and x86_64]
|
||||
- 11.0 # [osx and arm64]
|
||||
hdf5:
|
||||
- 1.14.3
|
||||
- 1.14.6
|
||||
libboost_devel:
|
||||
- '1.84'
|
||||
- '1.86'
|
||||
libxml2:
|
||||
- '2'
|
||||
mpfr:
|
||||
@@ -47,12 +44,12 @@ target_platform:
|
||||
- win-64 # [win]
|
||||
- linux-64 # [linux]
|
||||
- osx-64 # [osx]
|
||||
macos_machine:
|
||||
- x86_64-apple-darwin13.4.0
|
||||
MACOSX_DEPLOYMENT_TARGET:
|
||||
- '10.13'
|
||||
MACOSX_SDK_VERSION:
|
||||
- '10.13'
|
||||
macos_machine: # [osx]
|
||||
- x86_64-apple-darwin13.4.0 # [osx and x86_64]
|
||||
- arm64-apple-darwin20.0.0 # [osx and arm64]
|
||||
MACOSX_DEPLOYMENT_TARGET: # [osx]
|
||||
- 11.0 # [osx and arm64]
|
||||
- 10.13 # [osx and x86_64]
|
||||
|
||||
CONDA_BUILD_SYSROOT:
|
||||
- "/Users/runner/work/MacOSX10.13.sdk" # [osx]
|
||||
CONDA_BUILD_SYSROOT: # [osx]
|
||||
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
|
||||
@@ -1,64 +1,78 @@
|
||||
{% set version = environ.get('VERSION_OVERRIDE', '0.8.0') %}
|
||||
{% set build = 0 %}
|
||||
|
||||
# Higher number -> Always prioritize "novtk" variant over "all" variant
|
||||
{% set build = build + 200 %} # [variant == "novtk"]
|
||||
{% set build = build + 100 %} # [variant == "all"]
|
||||
context:
|
||||
version: ${{ env.get("VERSION_OVERRIDE", default="0.8.2.1") }}
|
||||
build: 1
|
||||
|
||||
package:
|
||||
name: ifcopenshell
|
||||
version: {{ version }}
|
||||
version: ${{ version }}
|
||||
|
||||
source:
|
||||
path: ../
|
||||
- path: ..
|
||||
|
||||
|
||||
build:
|
||||
number: {{ build }}
|
||||
skip: true # [py<39]
|
||||
binary_relocation: false # [osx]
|
||||
string: py{{ CONDA_PY }}_{{ variant }}_h{{ PKG_HASH }}_{{ build }}
|
||||
run_exports:
|
||||
- {{ pin_subpackage('ifcopenshell', max_pin='x.x.x') }} *{{ variant }}*
|
||||
|
||||
number: ${{ build }}
|
||||
dynamic_linking:
|
||||
binary_relocation: ${{ true if osx }}
|
||||
|
||||
requirements:
|
||||
build:
|
||||
- cmake
|
||||
- if: build_platform != target_platform
|
||||
then:
|
||||
- python
|
||||
- cross-python_${{ target_platform }}
|
||||
- cmake <4
|
||||
- ninja
|
||||
- swig >=4.1.1
|
||||
- {{ stdlib("c") }}
|
||||
- {{ compiler('c') }}
|
||||
- {{ compiler('cxx') }}
|
||||
- ${{ stdlib("c") }}
|
||||
- ${{ compiler('c') }}
|
||||
- ${{ compiler('cxx') }}
|
||||
host:
|
||||
- python
|
||||
- libboost-devel
|
||||
- occt *=*{{ variant }}*
|
||||
- occt
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
- eigen
|
||||
- mpfr
|
||||
- nlohmann_json
|
||||
- gmp # [unix]
|
||||
- mpir # [win]
|
||||
- gmp
|
||||
- zlib
|
||||
run:
|
||||
- python
|
||||
- {{ pin_compatible('occt', max_pin='x.x.x') }} *{{ variant }}*
|
||||
- {{ pin_compatible('cgal-cpp', max_pin='x.x') }}
|
||||
|
||||
|
||||
test:
|
||||
imports:
|
||||
- ifcopenshell
|
||||
- shapely
|
||||
- typing_extensions
|
||||
- ${{ pin_compatible('occt', upper_bound='x.x.x') }}
|
||||
- ${{ pin_compatible('cgal-cpp', upper_bound='x.x') }}
|
||||
run_exports:
|
||||
- ${{ pin_subpackage('ifcopenshell', upper_bound='x.x.x') }}
|
||||
|
||||
tests:
|
||||
- python:
|
||||
imports:
|
||||
- ifcopenshell
|
||||
pip_check: false
|
||||
- script:
|
||||
- python -c "import ifcopenshell; assert ifcopenshell.version == '${{ version }}', 'print(ifcopenshell.version)'"
|
||||
requirements:
|
||||
run:
|
||||
- occt * *novtk* # Ensure that even though compiled against OCCT with VTK, it can still run with OCCT without VTK
|
||||
- pytest
|
||||
- python-dateutil
|
||||
- xmlschema
|
||||
- xsdata
|
||||
- lxml
|
||||
- isodate
|
||||
- lark
|
||||
- networkx
|
||||
- tabulate
|
||||
- shapely
|
||||
|
||||
about:
|
||||
home: https://ifcopenshell.org
|
||||
license: LGPL-3.0-or-later
|
||||
license_file: COPYING
|
||||
summary: 'IfcOpenShell is a library to support the IFC file format'
|
||||
summary: IfcOpenShell is a library to support the IFC file format
|
||||
description: |
|
||||
IfcOpenShell
|
||||
============
|
||||
@@ -293,8 +307,9 @@ about:
|
||||
[MSYS2]: https://msys2.github.io/ "MSYS2"
|
||||
[win/readme.md]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/win/readme.md "win/readme.md"
|
||||
[nix/build-all.py]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/nix/build-all.py "nix/build-all.py"
|
||||
doc_url: https://ifcopenshell.org/
|
||||
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
homepage: https://ifcopenshell.org
|
||||
repository: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
documentation: https://ifcopenshell.org/
|
||||
|
||||
extra:
|
||||
recipe-maintainers:
|
||||
@@ -0,0 +1,32 @@
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def update_version(file_path: str, version: str) -> None:
|
||||
"""Update the version string in the given __init__.py file."""
|
||||
file_path = Path(file_path)
|
||||
|
||||
# Read the file and replace the version
|
||||
file_contents = file_path.read_text(encoding="utf-8")
|
||||
new_contents = re.sub(r'version = "0\.0\.0"', f'version = "{version}"', file_contents)
|
||||
|
||||
# Write the updated contents back to the file
|
||||
file_path.write_text(new_contents, encoding="utf-8")
|
||||
|
||||
print(f"Updated version in {file_path} to {version}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Update the version string in a Python file.")
|
||||
parser.add_argument(
|
||||
"version",
|
||||
type=str,
|
||||
help="The version string to replace '0.0.0' with (e.g., '1.2.3')."
|
||||
)
|
||||
parser.add_argument(
|
||||
"file",
|
||||
type=str,
|
||||
help="The path to the __init__.py file where the version will be updated."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
update_version(args.file, args.version)
|
||||
@@ -35,3 +35,6 @@ extend-exclude = '''
|
||||
[tool.pyright]
|
||||
reportInvalidTypeForm = false
|
||||
disableBytesTypePromotions = true
|
||||
reportUnnecessaryTypeIgnoreComment = true
|
||||
# Just to add a quick insert of `# pyright: ignore[xxx]` comments in Pylance.
|
||||
enableTypeIgnoreComments = false
|
||||
|
||||
@@ -20,7 +20,7 @@ def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData
|
||||
- subattribute name"""
|
||||
possible_attributes = {}
|
||||
for field in fields(type(extensions)):
|
||||
field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue]
|
||||
field_type = field.type.__args__[0] # pyright: ignore [reportAttributeAccessIssue]
|
||||
subfield = next(iter(fields(field_type)))
|
||||
xsd_name = subfield.metadata["name"]
|
||||
possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
SHELL := sh
|
||||
PYTHON:=python3.11
|
||||
PIP:=pip3.11
|
||||
PATCH:=patch
|
||||
|
||||
@@ -28,6 +28,10 @@ IN_BLENDER = sys.modules.get("bpy", None)
|
||||
if IN_BLENDER:
|
||||
import bpy
|
||||
|
||||
# This file is executed twice - first as a bonsai-extension
|
||||
# and then as a bonsai-package.
|
||||
IN_PACKAGE = __package__ == "bonsai"
|
||||
|
||||
import re
|
||||
import platform
|
||||
import traceback
|
||||
@@ -218,17 +222,21 @@ if IN_BLENDER:
|
||||
info["binary_python_version"] = version
|
||||
return info
|
||||
|
||||
try:
|
||||
import git
|
||||
def update_commit_data() -> None:
|
||||
try:
|
||||
import git
|
||||
|
||||
# We can't just use __file__ as bonsai/__init__.py is typically not symlinked
|
||||
# as Blender have errors symlinking main addon package file.
|
||||
path = Path(__file__).resolve().parent
|
||||
repo = git.Repo(str(path), search_parent_directories=True)
|
||||
last_commit_hash = repo.head.object.hexsha
|
||||
last_commit_date = repo.head.object.committed_datetime.isoformat()
|
||||
except:
|
||||
pass
|
||||
global last_commit_hash
|
||||
global last_commit_date
|
||||
path = Path(__file__).resolve().parent
|
||||
repo = git.Repo(str(path), search_parent_directories=True)
|
||||
last_commit_hash = repo.head.object.hexsha
|
||||
last_commit_date = repo.head.object.committed_datetime.isoformat()
|
||||
except:
|
||||
pass
|
||||
|
||||
if IN_PACKAGE:
|
||||
update_commit_data()
|
||||
|
||||
try:
|
||||
import ifcopenshell.api
|
||||
@@ -244,6 +252,12 @@ if IN_BLENDER:
|
||||
|
||||
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
|
||||
|
||||
def purge_cache():
|
||||
"""Purge cache left from previous session (e.g. after reload or update)."""
|
||||
import bonsai.tool as tool
|
||||
|
||||
tool.Blender.get_bonsai_version.cache_clear()
|
||||
|
||||
def register():
|
||||
if platform.system() == "Windows":
|
||||
clean_up_dlls_safe_links()
|
||||
@@ -261,6 +275,7 @@ if IN_BLENDER:
|
||||
bonsai.REINSTALLED_BBIM_VERSION = current_version
|
||||
|
||||
bonsai.bim.register()
|
||||
purge_cache()
|
||||
|
||||
def unregister():
|
||||
if platform.system() == "Windows":
|
||||
|
||||
@@ -101,6 +101,7 @@ classes = [
|
||||
operator.BIM_OT_delete_object,
|
||||
operator.BIM_OT_remove_section_plane,
|
||||
operator.BIM_OT_select_entity,
|
||||
operator.BIM_OT_select_entity_by_guid,
|
||||
operator.BIM_OT_select_object,
|
||||
operator.BIM_OT_show_description,
|
||||
operator.BIM_OT_multiple_file_selector,
|
||||
@@ -122,6 +123,7 @@ classes = [
|
||||
operator.SelectURIAttribute,
|
||||
operator.SetTab,
|
||||
operator.SwitchTab,
|
||||
operator.ShowSystemInfo,
|
||||
prop.StrProperty,
|
||||
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
|
||||
prop.ObjProperty,
|
||||
|
||||
@@ -47,8 +47,6 @@
|
||||
"scene.render.simplify_gpencil_shader_fx": true,
|
||||
"scene.render.simplify_gpencil_tint": true,
|
||||
"scene.render.simplify_gpencil_view_fill": true,
|
||||
"scene.render.simplify_shadows": 1.0,
|
||||
"scene.render.simplify_shadows_render": 1.0,
|
||||
"scene.render.simplify_subdivision": 6,
|
||||
"scene.render.simplify_subdivision_render": 6,
|
||||
"scene.render.simplify_volumes": 1.0,
|
||||
@@ -147,7 +145,7 @@
|
||||
"scene.display.shading.use_scene_world_render": false,
|
||||
"scene.display.shading.use_studiolight_view_rotation": true,
|
||||
"scene.display.shading.use_world_space_lighting": false,
|
||||
"scene.display.shading.wireframe_color_type": "MATERIAL",
|
||||
"scene.display.shading.wireframe_color_type": "THEME",
|
||||
"scene.display.shading.xray_alpha": 0.5,
|
||||
"scene.display.shading.xray_alpha_wireframe": 0.0,
|
||||
"scene.display.light_direction": [
|
||||
@@ -162,7 +160,6 @@
|
||||
"scene.display.shadow_focus": 0.0,
|
||||
"scene.display.shadow_shift": 0.10000000149011612,
|
||||
"scene.display.viewport_aa": "FXAA",
|
||||
"space.overlay.backwire_opacity": 0.5,
|
||||
"space.overlay.bone_wire_alpha": 1.0,
|
||||
"space.overlay.display_handle": "SELECTED",
|
||||
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
|
||||
@@ -189,7 +186,6 @@
|
||||
"space.overlay.show_edge_crease": true,
|
||||
"space.overlay.show_edge_seams": true,
|
||||
"space.overlay.show_edge_sharp": true,
|
||||
"space.overlay.show_edges": false,
|
||||
"space.overlay.show_extra_edge_angle": false,
|
||||
"space.overlay.show_extra_edge_length": false,
|
||||
"space.overlay.show_extra_face_angle": false,
|
||||
@@ -208,7 +204,6 @@
|
||||
"space.overlay.show_motion_paths": true,
|
||||
"space.overlay.show_object_origins": false,
|
||||
"space.overlay.show_object_origins_all": false,
|
||||
"space.overlay.show_occlude_wire": false,
|
||||
"space.overlay.show_onion_skins": false,
|
||||
"space.overlay.show_ortho_grid": true,
|
||||
"space.overlay.show_outline_selected": true,
|
||||
@@ -298,8 +293,6 @@
|
||||
"scene.render.simplify_gpencil_shader_fx": true,
|
||||
"scene.render.simplify_gpencil_tint": true,
|
||||
"scene.render.simplify_gpencil_view_fill": true,
|
||||
"scene.render.simplify_shadows": 1.0,
|
||||
"scene.render.simplify_shadows_render": 1.0,
|
||||
"scene.render.simplify_subdivision": 6,
|
||||
"scene.render.simplify_subdivision_render": 6,
|
||||
"scene.render.simplify_volumes": 1.0,
|
||||
@@ -398,7 +391,7 @@
|
||||
"scene.display.shading.use_scene_world_render": false,
|
||||
"scene.display.shading.use_studiolight_view_rotation": true,
|
||||
"scene.display.shading.use_world_space_lighting": false,
|
||||
"scene.display.shading.wireframe_color_type": "MATERIAL",
|
||||
"scene.display.shading.wireframe_color_type": "THEME",
|
||||
"scene.display.shading.xray_alpha": 0.5,
|
||||
"scene.display.shading.xray_alpha_wireframe": 0.0,
|
||||
"scene.display.light_direction": [
|
||||
@@ -413,7 +406,6 @@
|
||||
"scene.display.shadow_focus": 0.0,
|
||||
"scene.display.shadow_shift": 0.10000000149011612,
|
||||
"scene.display.viewport_aa": "FXAA",
|
||||
"space.overlay.backwire_opacity": 0.5,
|
||||
"space.overlay.bone_wire_alpha": 1.0,
|
||||
"space.overlay.display_handle": "SELECTED",
|
||||
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
|
||||
@@ -440,7 +432,6 @@
|
||||
"space.overlay.show_edge_crease": true,
|
||||
"space.overlay.show_edge_seams": true,
|
||||
"space.overlay.show_edge_sharp": true,
|
||||
"space.overlay.show_edges": false,
|
||||
"space.overlay.show_extra_edge_angle": false,
|
||||
"space.overlay.show_extra_edge_length": false,
|
||||
"space.overlay.show_extra_face_angle": false,
|
||||
@@ -459,7 +450,6 @@
|
||||
"space.overlay.show_motion_paths": true,
|
||||
"space.overlay.show_object_origins": false,
|
||||
"space.overlay.show_object_origins_all": false,
|
||||
"space.overlay.show_occlude_wire": false,
|
||||
"space.overlay.show_onion_skins": false,
|
||||
"space.overlay.show_ortho_grid": true,
|
||||
"space.overlay.show_outline_selected": true,
|
||||
@@ -520,7 +510,7 @@
|
||||
"scene.render.border_min_x": 0.0,
|
||||
"scene.render.border_min_y": 0.0,
|
||||
"scene.render.dither_intensity": 1.0,
|
||||
"scene.render.engine": "BLENDER_EEVEE",
|
||||
"scene.render.engine": "BLENDER_EEVEE_NEXT",
|
||||
"scene.render.film_transparent": false,
|
||||
"scene.render.filter_size": 1.5,
|
||||
"scene.render.fps": 24,
|
||||
@@ -549,8 +539,6 @@
|
||||
"scene.render.simplify_gpencil_shader_fx": true,
|
||||
"scene.render.simplify_gpencil_tint": true,
|
||||
"scene.render.simplify_gpencil_view_fill": true,
|
||||
"scene.render.simplify_shadows": 1.0,
|
||||
"scene.render.simplify_shadows_render": 1.0,
|
||||
"scene.render.simplify_subdivision": 6,
|
||||
"scene.render.simplify_subdivision_render": 6,
|
||||
"scene.render.simplify_volumes": 1.0,
|
||||
@@ -649,7 +637,7 @@
|
||||
"scene.display.shading.use_scene_world_render": false,
|
||||
"scene.display.shading.use_studiolight_view_rotation": true,
|
||||
"scene.display.shading.use_world_space_lighting": false,
|
||||
"scene.display.shading.wireframe_color_type": "MATERIAL",
|
||||
"scene.display.shading.wireframe_color_type": "THEME",
|
||||
"scene.display.shading.xray_alpha": 0.5,
|
||||
"scene.display.shading.xray_alpha_wireframe": 0.0,
|
||||
"scene.display.light_direction": [
|
||||
@@ -664,7 +652,6 @@
|
||||
"scene.display.shadow_focus": 0.0,
|
||||
"scene.display.shadow_shift": 0.10000000149011612,
|
||||
"scene.display.viewport_aa": "FXAA",
|
||||
"space.overlay.backwire_opacity": 0.5,
|
||||
"space.overlay.bone_wire_alpha": 1.0,
|
||||
"space.overlay.display_handle": "SELECTED",
|
||||
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
|
||||
@@ -691,7 +678,6 @@
|
||||
"space.overlay.show_edge_crease": true,
|
||||
"space.overlay.show_edge_seams": true,
|
||||
"space.overlay.show_edge_sharp": true,
|
||||
"space.overlay.show_edges": false,
|
||||
"space.overlay.show_extra_edge_angle": false,
|
||||
"space.overlay.show_extra_edge_length": false,
|
||||
"space.overlay.show_extra_face_angle": false,
|
||||
@@ -710,7 +696,6 @@
|
||||
"space.overlay.show_motion_paths": true,
|
||||
"space.overlay.show_object_origins": true,
|
||||
"space.overlay.show_object_origins_all": false,
|
||||
"space.overlay.show_occlude_wire": false,
|
||||
"space.overlay.show_onion_skins": false,
|
||||
"space.overlay.show_ortho_grid": true,
|
||||
"space.overlay.show_outline_selected": true,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION((),'2;1');
|
||||
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset_Drawing',$);
|
||||
FILE_DESCRIPTION($,'2;1');
|
||||
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_Drawing',$);
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23,#24,#25,#26,#27));
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -14,7 +14,7 @@ DATA;
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
@@ -32,5 +32,6 @@ DATA;
|
||||
#25=IFCSIMPLEPROPERTYTEMPLATE('3LHwCrOcb6Y8ozfJZ7Ay$c',$,'LineworkMode','Method to use for line work',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -392,6 +392,7 @@ def draw_filter(
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{len(data.data['saved_searches'])} Saved Searches")
|
||||
|
||||
row.operator("bim.select_entity_by_guid", text="", icon="CON_OBJECTSOLVER")
|
||||
if data.data["saved_searches"]:
|
||||
row.operator("bim.load_search", text="", icon="IMPORT").module = module
|
||||
row.operator("bim.save_search", text="", icon="EXPORT").module = module
|
||||
|
||||
@@ -31,9 +31,10 @@ import ifcopenshell.ifcopenshell_wrapper
|
||||
import bonsai
|
||||
import bonsai.bim.handler
|
||||
import bonsai.tool as tool
|
||||
from ifcopenshell.file import UndoSystemError
|
||||
from pathlib import Path
|
||||
from bonsai.tool.brick import BrickStore
|
||||
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
|
||||
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, Literal
|
||||
|
||||
|
||||
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
|
||||
@@ -417,6 +418,32 @@ class IfcStore:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.ifc_definition_id = 0
|
||||
|
||||
@staticmethod
|
||||
def get_ifc_file_undo_callback(callback_type: Literal["UNDO", "REDO"]):
|
||||
def callback(_) -> None:
|
||||
try:
|
||||
if callback_type == "UNDO":
|
||||
tool.Ifc.get().undo()
|
||||
else:
|
||||
tool.Ifc.get().redo()
|
||||
except Exception as e:
|
||||
# Persistent callbacks errors are not visible from UI and we set `last_error`
|
||||
# to make it visible.
|
||||
error_msg = ""
|
||||
# In theory it should always be UndoSystemError, but just to be safe.
|
||||
if isinstance(e, UndoSystemError):
|
||||
error_msg += "Undo transaction operations:\n"
|
||||
transaction = e.transaction
|
||||
for operation in transaction.operations:
|
||||
error_msg += f"- {str(operation)}\n"
|
||||
# Show it in system console too, not just in last message.
|
||||
print(error_msg)
|
||||
error_msg += traceback.format_exc()
|
||||
bonsai.last_error = error_msg
|
||||
raise
|
||||
|
||||
return callback
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_operator(
|
||||
operator: tool.Ifc.Operator,
|
||||
@@ -446,7 +473,9 @@ class IfcStore:
|
||||
if tool.Ifc.get():
|
||||
tool.Ifc.get().end_transaction()
|
||||
IfcStore.add_transaction_operation(
|
||||
operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
|
||||
operator,
|
||||
rollback=IfcStore.get_ifc_file_undo_callback("UNDO"),
|
||||
commit=IfcStore.get_ifc_file_undo_callback("REDO"),
|
||||
)
|
||||
if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages.
|
||||
BrickStore.end_transaction()
|
||||
|
||||
@@ -38,7 +38,7 @@ import ifcopenshell.util.shape
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
|
||||
from bonsai.tool.loader import OBJECT_DATA_TYPE
|
||||
from typing import Dict, Union, Optional, Any, Literal
|
||||
from typing import Dict, Union, Optional, Any, Literal, Iterable
|
||||
from ifcopenshell.util.shape import MatrixType
|
||||
|
||||
|
||||
@@ -198,11 +198,16 @@ class MaterialCreator:
|
||||
|
||||
|
||||
class IfcImporter:
|
||||
file: ifcopenshell.file
|
||||
"""Either provided by user as an attribute or will be loaded from ``input_file`` during ``execute()``."""
|
||||
|
||||
elements: set[ifcopenshell.entity_instance]
|
||||
"""Set of IfcElements to import. Excluding ``gross_elements`` and ```native_elements``."""
|
||||
|
||||
def __init__(self, ifc_import_settings: IfcImportSettings):
|
||||
self.ifc_import_settings = ifc_import_settings
|
||||
tool.Loader.set_settings(ifc_import_settings)
|
||||
self.diff = None
|
||||
self.file: ifcopenshell.file = None
|
||||
self.project = None
|
||||
self.has_existing_project = False
|
||||
# element guids to blender collections mapping
|
||||
@@ -300,6 +305,7 @@ class IfcImporter:
|
||||
bpy.context.window_manager.progress_end()
|
||||
|
||||
def process_context_filter(self) -> None:
|
||||
"""Setup contexts. Necessary for importing elements representations."""
|
||||
contexts = self.file.by_type("IfcGeometricRepresentationContext")
|
||||
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
|
||||
print("Warning! Excessive contexts were found and merged where applicable.")
|
||||
@@ -319,16 +325,15 @@ class IfcImporter:
|
||||
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
||||
|
||||
def process_element_filter(self) -> None:
|
||||
elements: list[ifcopenshell.entity_instance]
|
||||
if self.ifc_import_settings.has_filter:
|
||||
self.elements = self.ifc_import_settings.elements
|
||||
if isinstance(self.elements, set):
|
||||
self.elements = list(self.elements)
|
||||
elements = list(self.ifc_import_settings.elements)
|
||||
# TODO: enable filtering for annotations
|
||||
else:
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
|
||||
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
|
||||
else:
|
||||
self.elements = self.file.by_type("IfcElement")
|
||||
elements = self.file.by_type("IfcElement")
|
||||
|
||||
drawing_groups = [g for g in self.file.by_type("IfcGroup") if g.ObjectType == "DRAWING"]
|
||||
drawing_annotations = set()
|
||||
@@ -338,16 +343,16 @@ class IfcImporter:
|
||||
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
|
||||
self.annotations -= drawing_annotations
|
||||
|
||||
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
|
||||
elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
|
||||
if self.ifc_import_settings.element_limit_mode == "UNLIMITED":
|
||||
self.elements = set(self.elements)
|
||||
self.elements = set(elements)
|
||||
else:
|
||||
offset = self.ifc_import_settings.element_offset
|
||||
offset_limit = offset + self.ifc_import_settings.element_limit
|
||||
self.elements = set(self.elements[offset:offset_limit])
|
||||
self.elements = set(elements[offset:offset_limit])
|
||||
|
||||
if self.ifc_import_settings.has_filter or self.ifc_import_settings.element_limit_mode != "UNLIMITED":
|
||||
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
|
||||
self.element_types = {t for e in self.elements if (t := ifcopenshell.util.element.get_type(e))}
|
||||
else:
|
||||
self.element_types = set(self.file.by_type("IfcTypeProduct"))
|
||||
|
||||
@@ -385,6 +390,7 @@ class IfcImporter:
|
||||
return results
|
||||
|
||||
def parse_native_elements(self) -> None:
|
||||
# TODO: move to `process_element_filter` to incapsulate all `self.elements` logic.
|
||||
if not self.ifc_import_settings.should_load_geometry:
|
||||
return
|
||||
if not self.file.by_type("IfcSweptDiskSolid"):
|
||||
@@ -422,7 +428,8 @@ class IfcImporter:
|
||||
|
||||
rep = representation
|
||||
while True:
|
||||
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
|
||||
# Tekla 2023 has missing items, though it's invalid IFC.
|
||||
if rep.Items and 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
|
||||
@@ -589,6 +596,7 @@ class IfcImporter:
|
||||
return self.create_generic_sqlite_elements(elements)
|
||||
|
||||
if self.ifc_import_settings.should_load_geometry:
|
||||
elements = elements.copy() # Prevent argument mutation.
|
||||
context_settings = (
|
||||
tool.Loader.settings.gross_context_settings if is_gross else tool.Loader.settings.context_settings
|
||||
)
|
||||
@@ -1130,7 +1138,22 @@ class IfcImporter:
|
||||
bpy.ops.view3d.view_selected()
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
def setup_arrays(self):
|
||||
def setup_arrays(
|
||||
self,
|
||||
*,
|
||||
annotations_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
|
||||
openings_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
|
||||
) -> None:
|
||||
# During base import some elements are skipped.
|
||||
is_base_import = True
|
||||
elements_to_import: set[ifcopenshell.entity_instance] = set()
|
||||
if annotations_to_import is not None:
|
||||
is_base_import = False
|
||||
elements_to_import = annotations_to_import
|
||||
elif openings_to_import is not None:
|
||||
is_base_import = False
|
||||
elements_to_import = openings_to_import
|
||||
|
||||
for pset in self.file.by_type("IfcPropertySet"):
|
||||
if pset.Name != "BBIM_Array":
|
||||
continue
|
||||
@@ -1138,16 +1161,24 @@ class IfcImporter:
|
||||
continue
|
||||
data = json.loads(data)
|
||||
for rel in pset.DefinesOccurrence:
|
||||
element: ifcopenshell.entity_instance
|
||||
for element in rel.RelatedObjects:
|
||||
if is_base_import:
|
||||
if element.is_a("IfcAnnotation") or element.is_a("IfcOpeningElement"):
|
||||
continue
|
||||
else:
|
||||
if element not in elements_to_import:
|
||||
continue
|
||||
for i in range(len(data)):
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
input_file: Union[str, None] = None
|
||||
logger: Union[logging.Logger, None] = None
|
||||
|
||||
def __init__(self):
|
||||
self.logger: logging.Logger = None
|
||||
self.input_file = None
|
||||
self.diff_file = None
|
||||
self.geometry_library = "opencascade"
|
||||
self.should_use_cpu_multiprocessing = True
|
||||
@@ -1168,17 +1199,19 @@ class IfcImportSettings:
|
||||
self.element_limit_mode = "UNLIMITED"
|
||||
self.element_offset = 0
|
||||
self.element_limit = 30000
|
||||
self.has_filter = None
|
||||
self.has_filter = False
|
||||
self.should_filter_spatial_elements = True
|
||||
self.should_setup_viewport_camera = True
|
||||
self.contexts: list[ifcopenshell.entity_instance] = []
|
||||
self.context_settings: list[ifcopenshell.geom.main.settings] = []
|
||||
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
|
||||
self.elements: set[ifcopenshell.entity_instance] = set()
|
||||
self.elements: Iterable[ifcopenshell.entity_instance] = set()
|
||||
self.load_indexed_maps = False
|
||||
|
||||
@staticmethod
|
||||
def factory(context=None, input_file=None, logger=None):
|
||||
def factory(
|
||||
context=None, input_file: Optional[str] = None, logger: Optional[logging.Logger] = None
|
||||
) -> IfcImportSettings:
|
||||
scene_diff = tool.Blender.get_diff_props()
|
||||
props = tool.Project.get_project_props()
|
||||
settings = IfcImportSettings()
|
||||
|
||||
@@ -164,8 +164,6 @@ class AggregateDecorator:
|
||||
|
||||
def draw_aggregate(self, context):
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
if props.in_aggregate_mode:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
decorator_color_special = self.addon_prefs.decorator_color_special
|
||||
decorator_color_selected = self.addon_prefs.decorator_color_selected
|
||||
@@ -194,7 +192,13 @@ class AggregateDecorator:
|
||||
aggregates.append(obj)
|
||||
continue
|
||||
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
aggregates_list = tool.Aggregate.get_aggregates_recursively(element)
|
||||
if props.in_aggregate_mode and props.editing_aggregate:
|
||||
index = aggregates_list.index(tool.Ifc.get_entity(props.editing_aggregate))
|
||||
if index > 0:
|
||||
aggregate = aggregates_list[index - 1]
|
||||
else:
|
||||
aggregate = aggregates_list[-1]
|
||||
if aggregate:
|
||||
aggregates.append(tool.Ifc.get_object(aggregate))
|
||||
|
||||
@@ -212,9 +216,7 @@ class AggregateDecorator:
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
if props.in_aggregate_mode:
|
||||
return
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
|
||||
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate))
|
||||
parts_objs = [tool.Ifc.get_object(p) for p in parts]
|
||||
|
||||
indices, edges = create_bounding_box(parts_objs)
|
||||
@@ -333,7 +335,7 @@ class AggregateModeDecorator:
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
|
||||
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate_obj))
|
||||
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
parts_objs = [tool.Ifc.get_object(p) for p in parts]
|
||||
|
||||
@@ -140,12 +140,11 @@ class BIM_OT_disable_editing_aggregate(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"""Add aggregate to IFC element"""
|
||||
"""Add aggregate to selected IFC elements."""
|
||||
|
||||
bl_idname = "bim.add_aggregate"
|
||||
bl_label = "Add Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
|
||||
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
|
||||
|
||||
@@ -165,7 +164,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
@@ -386,7 +385,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
||||
bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ class Objects(bpy.types.PropertyGroup):
|
||||
class BIMAggregateProperties(PropertyGroup):
|
||||
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
|
||||
editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
|
||||
previous_editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
|
||||
editing_objects: CollectionProperty(type=Objects)
|
||||
not_editing_objects: CollectionProperty(type=Objects)
|
||||
aggregate_decorator: BoolProperty(
|
||||
@@ -126,10 +127,16 @@ class BIMAggregateProperties(PropertyGroup):
|
||||
default=False,
|
||||
update=update_aggregate_decorator,
|
||||
)
|
||||
previous_state: BoolProperty(
|
||||
name="True if it was previously in aggregate mode",
|
||||
default=False,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
in_aggregate_mode: bool
|
||||
editing_aggregate: Union[bpy.types.Object, None]
|
||||
previous_editing_aggregate: Union[bpy.types.Object, None]
|
||||
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
aggregate_decorator: bool
|
||||
previous_state: bool
|
||||
|
||||
@@ -50,7 +50,8 @@ class BIM_PT_aggregate(Panel):
|
||||
row = layout.row()
|
||||
row.label(text="Aggregate Decorator")
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
row.prop(props, "aggregate_decorator", icon="HIDE_OFF", text="")
|
||||
icon = "HIDE_OFF" if props.aggregate_decorator else "HIDE_ON"
|
||||
row.prop(props, "aggregate_decorator", icon=icon, text="")
|
||||
if not AggregateData.is_loaded:
|
||||
AggregateData.load()
|
||||
|
||||
@@ -78,10 +79,8 @@ class BIM_PT_aggregate(Panel):
|
||||
if AggregateData.data["has_relating_object"]:
|
||||
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
|
||||
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
|
||||
op.obj = context.active_object.name
|
||||
op.select_parts = False
|
||||
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.obj = context.active_object.name
|
||||
op.select_parts = True
|
||||
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.add_aggregate", icon="ADD", text="")
|
||||
|
||||
@@ -1283,6 +1283,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
cam_aspect: float,
|
||||
context: bpy.types.Context,
|
||||
) -> None:
|
||||
assert isinstance(obj.data, bpy.types.Camera)
|
||||
if viewpoint.visualization_info.orthogonal_camera:
|
||||
camera = viewpoint.visualization_info.orthogonal_camera
|
||||
obj.data.type = "ORTHO"
|
||||
@@ -1400,6 +1401,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
self.hide_spaces(context)
|
||||
|
||||
def hide_spaces(self, context: bpy.types.Context) -> None:
|
||||
assert context.area
|
||||
old = context.area.type
|
||||
context.area.type = "VIEW_3D"
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
@@ -21,14 +21,13 @@ from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.GetBSDDClassificationProperties,
|
||||
operator.LoadBSDDDomains,
|
||||
operator.LoadBSDDDictionaries,
|
||||
operator.SearchBSDDClass,
|
||||
operator.SetActiveBSDDDictionary,
|
||||
prop.BSDDDomain,
|
||||
prop.BSDDDictionary,
|
||||
prop.BSDDClassification,
|
||||
prop.BSDDPset,
|
||||
prop.BIMBSDDProperties,
|
||||
ui.BIM_UL_bsdd_domains,
|
||||
ui.BIM_UL_bsdd_dictionaries,
|
||||
ui.BIM_UL_bsdd_classifications,
|
||||
ui.BIM_PT_bsdd,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.date
|
||||
import ifcopenshell.util.classification
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def refresh():
|
||||
BSDDData.is_loaded = False
|
||||
|
||||
|
||||
class BSDDData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["active_dictionary"] = cls.active_dictionary()
|
||||
|
||||
@classmethod
|
||||
def active_dictionary(cls):
|
||||
props = tool.Bsdd.get_bsdd_props()
|
||||
results = [("ALL", "All Dictionaries", "All active dictionaries")]
|
||||
results.extend([(d.uri, d.name, f"{d.status} - {d.version}") for d in props.dictionaries if d.is_active])
|
||||
return results
|
||||
@@ -22,25 +22,13 @@ import bonsai.tool as tool
|
||||
from bonsai.core import bsdd as core
|
||||
|
||||
|
||||
class LoadBSDDDomains(bpy.types.Operator):
|
||||
bl_idname = "bim.load_bsdd_domains"
|
||||
class LoadBSDDDictionaries(bpy.types.Operator):
|
||||
bl_idname = "bim.load_bsdd_dictionaries"
|
||||
bl_label = "Load bSDD Dictionaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
core.load_bsdd(bsdd.Client(), tool.Bsdd)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetActiveBSDDDictionary(bpy.types.Operator):
|
||||
bl_idname = "bim.set_active_bsdd_domain"
|
||||
bl_label = "Set bSDD Dictionary as active"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
uri: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
core.set_active_bsdd_dictionary(self.name, self.uri, tool.Bsdd)
|
||||
core.load_bsdd(tool.Bsdd)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -50,17 +38,9 @@ class SearchBSDDClass(bpy.types.Operator):
|
||||
bl_description = "Search for bSDD classes by the provided keyword"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Requirement by buildingSMART bSDD api.
|
||||
if len(context.scene.BIMBSDDProperties.keyword) < 3:
|
||||
cls.poll_message_set("Search query has to be at least 3 characters long.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
keyword = context.scene.BIMBSDDProperties.keyword
|
||||
classes_found = core.search_class(keyword, bsdd.Client(), tool.Bsdd)
|
||||
classes_found = core.search_class(keyword, tool.Bsdd)
|
||||
self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -72,7 +52,7 @@ class GetBSDDClassificationProperties(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
pset_data = core.get_class_properties(bsdd.Client(), tool.Bsdd)
|
||||
pset_data = core.get_class_properties(tool.Bsdd)
|
||||
psets_found = len(pset_data)
|
||||
props_found = sum(len(pset) for pset in pset_data.values())
|
||||
self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.")
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bonsai.bim.module.bsdd.data import BSDDData
|
||||
from bonsai.bim.prop import Attribute, StrProperty
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -31,21 +32,34 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
class BSDDDomain(PropertyGroup):
|
||||
def get_active_dictionary(self, context):
|
||||
if not BSDDData.is_loaded:
|
||||
BSDDData.load()
|
||||
return BSDDData.data["active_dictionary"]
|
||||
|
||||
|
||||
def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None:
|
||||
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
|
||||
|
||||
|
||||
class BSDDDictionary(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
uri: StringProperty(name="URI")
|
||||
default_language_code: StringProperty(name="Language")
|
||||
organization_name_owner: StringProperty(name="Organization")
|
||||
status: StringProperty(name="Status")
|
||||
version: StringProperty(name="Version")
|
||||
is_active: BoolProperty(
|
||||
name="Is Active", description="Enable to search with this dictionary", default=False, update=update_is_active
|
||||
)
|
||||
|
||||
|
||||
class BSDDClassification(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
reference_code: StringProperty(name="Reference Code")
|
||||
uri: StringProperty(name="Namespace URI")
|
||||
domain_name: StringProperty(name="Domain Name")
|
||||
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
|
||||
dictionary_name: StringProperty(name="Dictionary Name")
|
||||
dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI")
|
||||
|
||||
|
||||
class BSDDPset(PropertyGroup):
|
||||
@@ -54,10 +68,11 @@ class BSDDPset(PropertyGroup):
|
||||
|
||||
|
||||
class BIMBSDDProperties(PropertyGroup):
|
||||
active_domain: StringProperty(name="Active Domain")
|
||||
active_dictionary: StringProperty(name="Active Dictionary")
|
||||
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
|
||||
active_uri: StringProperty(name="Active URI")
|
||||
domains: CollectionProperty(name="Domains", type=BSDDDomain)
|
||||
active_domain_index: IntProperty(name="Active Domain Index")
|
||||
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
|
||||
active_dictionary_index: IntProperty(name="Active Dictionary Index")
|
||||
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
|
||||
active_classification_index: IntProperty(name="Active Classification Index")
|
||||
keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive")
|
||||
@@ -69,9 +84,15 @@ class BIMBSDDProperties(PropertyGroup):
|
||||
use_only_ifc_properties: BoolProperty(
|
||||
name="Only IFC Properties",
|
||||
description="Whether to display and assign only properties from IFC dictionary",
|
||||
default=True,
|
||||
default=False,
|
||||
)
|
||||
load_preview_domains: BoolProperty(
|
||||
name="Load Preview Domains", description="Whether it should load preview and inactive domains", default=False
|
||||
load_preview_dictionaries: BoolProperty(
|
||||
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
|
||||
)
|
||||
load_inactive_dictionaries: BoolProperty(
|
||||
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
|
||||
)
|
||||
load_test_dictionaries: BoolProperty(
|
||||
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
|
||||
)
|
||||
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.bsdd.data import BSDDData
|
||||
from bpy.types import Panel, UIList
|
||||
|
||||
|
||||
@@ -30,65 +31,58 @@ class BIM_PT_bsdd(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
def draw(self, context):
|
||||
if not BSDDData.is_loaded:
|
||||
BSDDData.load()
|
||||
props = context.scene.BIMBSDDProperties
|
||||
layout = self.layout
|
||||
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:
|
||||
if len(props.dictionaries):
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + props.active_domain, icon="URL")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH")
|
||||
|
||||
if len(props.domains):
|
||||
if len(props.dictionaries):
|
||||
self.layout.template_list(
|
||||
"BIM_UL_bsdd_domains",
|
||||
"BIM_UL_bsdd_dictionaries",
|
||||
"",
|
||||
props,
|
||||
"domains",
|
||||
"dictionaries",
|
||||
props,
|
||||
"active_domain_index",
|
||||
"active_dictionary_index",
|
||||
)
|
||||
if 0 <= props.active_domain_index < len(props.domains):
|
||||
selected_domain = props.domains[props.active_domain_index]
|
||||
if 0 <= props.active_dictionary_index < len(props.dictionaries):
|
||||
selected_dictionary = props.dictionaries[props.active_dictionary_index]
|
||||
else:
|
||||
selected_domain = None
|
||||
selected_dictionary = None
|
||||
|
||||
if selected_domain:
|
||||
layout.label(text="Selected domain:")
|
||||
if selected_dictionary:
|
||||
layout.label(text="Selected dictionary:")
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row.label(text="Language")
|
||||
row.label(text=selected_domain.default_language_code)
|
||||
row.label(text=selected_dictionary.default_language_code)
|
||||
row = box.row(align=True)
|
||||
row.label(text="Version")
|
||||
row.label(text=selected_domain.version)
|
||||
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_domain.uri
|
||||
row.label(text=selected_dictionary.version)
|
||||
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_dictionary.uri
|
||||
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_bsdd_domains")
|
||||
row.operator("bim.load_bsdd_dictionaries")
|
||||
|
||||
|
||||
class BIM_UL_bsdd_domains(UIList):
|
||||
class BIM_UL_bsdd_dictionaries(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
props = context.scene.BIMBSDDProperties
|
||||
row = layout.row(align=True)
|
||||
if item.status != "Active":
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR")
|
||||
row.label(
|
||||
text=f"{item.name} ({item.organization_name_owner}) v{item.version} - {item.status}", icon="ERROR"
|
||||
)
|
||||
else:
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner})")
|
||||
if item.uri == props.active_uri:
|
||||
row.label(text="", icon="URL")
|
||||
else:
|
||||
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.name = item.name
|
||||
op.uri = item.uri
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner}) v{item.version}")
|
||||
row.prop(
|
||||
item, "is_active", icon="CHECKBOX_HLT" if item.is_active else "CHECKBOX_DEHLT", text="", emboss=False
|
||||
)
|
||||
|
||||
|
||||
class BIM_UL_bsdd_classifications(UIList):
|
||||
|
||||
@@ -116,28 +116,28 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
domain = next((d for d in props.domains if d.name == props.active_domain and d.uri == props.active_uri), None)
|
||||
|
||||
# Maybe user loaded preview domains, set it as active
|
||||
# and then reloaded them without preview domains.
|
||||
if not domain:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Couldn't find domain '{props.active_domain}' ({props.active_uri}). Try to reload bSDD dictionaries.",
|
||||
if props.active_dictionary == "ALL":
|
||||
dictionaries = [d.uri for d in props.dictionaries if d.is_active]
|
||||
else:
|
||||
dictionaries = [props.active_dictionary]
|
||||
for uri in dictionaries:
|
||||
if not (dictionary := tool.Bsdd.get_dictionary(uri)):
|
||||
continue
|
||||
has_classification = False
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == dictionary["name"] or (
|
||||
tool.Classification.get_location(element) == dictionary["uri"]
|
||||
):
|
||||
self.report({"INFO"}, f"Classification '{dictionary['name']}' is already added to the project.")
|
||||
has_classification = True
|
||||
if has_classification:
|
||||
continue
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=dictionary["name"]
|
||||
)
|
||||
return
|
||||
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == props.active_domain or (tool.Classification.get_location(element) == domain.uri):
|
||||
self.report({"INFO"}, f"Classification '{props.active_domain}' is already added to the project.")
|
||||
return
|
||||
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
|
||||
)
|
||||
classification.Source = domain.organization_name_owner
|
||||
classification.Edition = domain.version
|
||||
tool.Classification.set_location(classification, domain.uri)
|
||||
classification.Source = dictionary["organizationNameOwner"]
|
||||
classification.Edition = dictionary["version"]
|
||||
tool.Classification.set_location(classification, dictionary["uri"])
|
||||
|
||||
|
||||
class EnableAddingManualClassification(bpy.types.Operator):
|
||||
@@ -420,17 +420,17 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
classification = None
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == bsdd_classification.domain_name or (
|
||||
tool.Classification.get_location(element) == bsdd_classification.domain_namespace_uri
|
||||
if element.Name == bsdd_classification.dictionary_name or (
|
||||
tool.Classification.get_location(element) == bsdd_classification.dictionary_namespace_uri
|
||||
):
|
||||
classification = element
|
||||
break
|
||||
|
||||
if not classification:
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
|
||||
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.dictionary_name
|
||||
)
|
||||
tool.Classification.set_location(classification, bsdd_classification.domain_namespace_uri)
|
||||
tool.Classification.set_location(classification, bsdd_classification.dictionary_namespace_uri)
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
|
||||
|
||||
@@ -78,13 +78,8 @@ class BIM_PT_classifications(Panel):
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
self.bprops = context.scene.BIMBSDDProperties
|
||||
|
||||
if not self.bprops.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
|
||||
row.prop(self.bprops, "active_dictionary", text="")
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_classification_from_bsdd", icon="ADD")
|
||||
@@ -163,13 +158,8 @@ class ReferenceUI:
|
||||
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:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
|
||||
row.prop(self.bprops, "active_dictionary", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, "keyword", text="")
|
||||
|
||||
@@ -30,6 +30,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class BIMContextProperties(PropertyGroup):
|
||||
@@ -66,3 +67,10 @@ class BIMContextProperties(PropertyGroup):
|
||||
)
|
||||
active_context_id: IntProperty(name="Active Context Id")
|
||||
context_attributes: CollectionProperty(name="Context Attributes", type=Attribute)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
contexts: str
|
||||
subcontexts: str
|
||||
target_views: str
|
||||
active_context_id: int
|
||||
context_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
|
||||
@@ -33,13 +33,14 @@ class BIM_PT_context(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
def draw(self, context):
|
||||
if not ContextData.is_loaded:
|
||||
ContextData.load()
|
||||
|
||||
props = context.scene.BIMContextProperties
|
||||
assert self.layout
|
||||
props = tool.Context.get_context_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "contexts", text="")
|
||||
|
||||
@@ -61,6 +61,7 @@ classes = (
|
||||
operator.ExportCostSchedules,
|
||||
operator.HighlightProductCostItem,
|
||||
operator.ImportCostScheduleCsv,
|
||||
operator.RefreshCostScheduleCsv,
|
||||
operator.LoadCostItemElementQuantities,
|
||||
operator.LoadCostItemQuantities,
|
||||
operator.LoadCostItemResourceQuantities,
|
||||
@@ -82,6 +83,7 @@ classes = (
|
||||
prop.CostItem,
|
||||
prop.CostItemQuantity,
|
||||
prop.CostItemType,
|
||||
prop.CostItemsMapping,
|
||||
prop.ScheduleColumn,
|
||||
prop.BIMCostProperties,
|
||||
ui.BIM_PT_cost_schedules,
|
||||
|
||||
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -33,13 +33,19 @@ def refresh():
|
||||
CostItemQuantitiesData.is_loaded = False
|
||||
|
||||
|
||||
CostItem = dict[str, Any]
|
||||
CostQuantity = dict[str, Any]
|
||||
CostSchedule = dict[str, Any]
|
||||
Currency = dict[str, Any]
|
||||
|
||||
|
||||
class CostSchedulesData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
_cost_values: dict[int, dict[str, Any]]
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
def load(cls) -> None:
|
||||
cls.data = {
|
||||
"predefined_types": cls.get_cost_schedule_types(),
|
||||
"total_cost_schedules": cls.total_cost_schedules(),
|
||||
@@ -54,18 +60,18 @@ class CostSchedulesData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def currency(cls):
|
||||
def currency(cls) -> Union[Currency, None]:
|
||||
unit = tool.Unit.get_project_currency_unit()
|
||||
if unit:
|
||||
return {"id": unit.id(), "name": unit.Currency}
|
||||
|
||||
@classmethod
|
||||
def total_cost_schedules(cls):
|
||||
def total_cost_schedules(cls) -> int:
|
||||
return len(tool.Ifc.get().by_type("IfcCostSchedule"))
|
||||
|
||||
@classmethod
|
||||
def schedules(cls):
|
||||
results = []
|
||||
def schedules(cls) -> list[CostSchedule]:
|
||||
results: list[CostSchedule] = []
|
||||
props = tool.Cost.get_cost_props()
|
||||
if props.active_cost_schedule_id:
|
||||
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
|
||||
@@ -88,19 +94,19 @@ class CostSchedulesData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def is_editing_rates(cls):
|
||||
def is_editing_rates(cls) -> bool:
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_schedule_id
|
||||
if not ifc_id:
|
||||
return
|
||||
return False
|
||||
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
|
||||
|
||||
@classmethod
|
||||
def cost_items(cls):
|
||||
def cost_items(cls) -> dict[int, CostItem]:
|
||||
cls._cost_values = {}
|
||||
results = {}
|
||||
results: dict[int, CostItem] = {}
|
||||
for cost_item in tool.Ifc.get().by_type("IfcCostItem"):
|
||||
data = {}
|
||||
data: CostItem = {}
|
||||
cls._load_cost_item_quantities(cost_item, data)
|
||||
cls._load_cost_values(cost_item, data)
|
||||
cls._load_nesting_index(cost_item, data)
|
||||
@@ -108,13 +114,13 @@ class CostSchedulesData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def _load_nesting_index(cls, cost_item, data):
|
||||
def _load_nesting_index(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
|
||||
data["NestingIndex"] = None
|
||||
for rel in cost_item.Nests or []:
|
||||
data["NestingIndex"] = rel.RelatedObjects.index(cost_item)
|
||||
|
||||
@classmethod
|
||||
def _load_cost_values(cls, root_element, data):
|
||||
def _load_cost_values(cls, root_element: ifcopenshell.entity_instance, data: CostItem) -> None:
|
||||
# data["CostValues"] = []
|
||||
data["CategoryValues"] = {}
|
||||
data["UnitBasisValueComponent"] = None
|
||||
@@ -123,6 +129,7 @@ class CostSchedulesData:
|
||||
data["TotalCost"] = 0.0
|
||||
has_unit_basis = False
|
||||
is_sum = False
|
||||
values: list[ifcopenshell.entity_instance]
|
||||
if root_element.is_a("IfcCostItem"):
|
||||
values = root_element.CostValues
|
||||
elif root_element.is_a("IfcConstructionResource"):
|
||||
@@ -150,7 +157,7 @@ class CostSchedulesData:
|
||||
data["TotalAppliedValue"] = None
|
||||
|
||||
@classmethod
|
||||
def _load_cost_item_quantities(cls, cost_item, data):
|
||||
def _load_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
|
||||
# parametric_quantities = []
|
||||
# for rel in cost_item.Controls:
|
||||
# for related_object in rel.RelatedObjects or []:
|
||||
@@ -158,8 +165,9 @@ class CostSchedulesData:
|
||||
# parametric_quantities.extend(quantities)
|
||||
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
|
||||
data["UnitSymbol"] = "-"
|
||||
if cost_item.CostQuantities:
|
||||
quantity = cost_item.CostQuantities[0]
|
||||
quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
|
||||
if quantities:
|
||||
quantity = quantities[0]
|
||||
data["QuantityType"] = quantity.is_a()
|
||||
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
|
||||
if unit:
|
||||
@@ -213,23 +221,31 @@ class CostSchedulesData:
|
||||
) -> list[int]:
|
||||
if not element.is_a("IfcObject"):
|
||||
return []
|
||||
cost_quantities = cost_item.CostQuantities
|
||||
cost_quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
|
||||
if not cost_quantities:
|
||||
return []
|
||||
results = []
|
||||
|
||||
results: list[int] = []
|
||||
relationship: ifcopenshell.entity_instance
|
||||
for relationship in element.IsDefinedBy:
|
||||
if not relationship.is_a("IfcRelDefinesByProperties"):
|
||||
continue
|
||||
qto = relationship.RelatingPropertyDefinition
|
||||
qto: ifcopenshell.entity_instance = relationship.RelatingPropertyDefinition
|
||||
if not qto.is_a("IfcElementQuantity"):
|
||||
continue
|
||||
prop: ifcopenshell.entity_instance
|
||||
for prop in qto.Quantities:
|
||||
if prop in cost_quantities:
|
||||
results.append(prop.id())
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def _load_cost_value(cls, root_element, root_element_data, cost_value):
|
||||
def _load_cost_value(
|
||||
cls,
|
||||
root_element: ifcopenshell.entity_instance,
|
||||
root_element_data: CostItem,
|
||||
cost_value: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
value_data = cost_value.get_info()
|
||||
del value_data["AppliedValue"]
|
||||
if value_data["UnitBasis"]:
|
||||
@@ -256,8 +272,8 @@ class CostSchedulesData:
|
||||
cls._load_cost_value(root_element, root_element_data, component)
|
||||
|
||||
@classmethod
|
||||
def cost_quantities(cls):
|
||||
results = []
|
||||
def cost_quantities(cls) -> list[CostQuantity]:
|
||||
results: list[CostQuantity] = []
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_item_id
|
||||
if not ifc_id:
|
||||
@@ -267,7 +283,7 @@ class CostSchedulesData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def cost_values(cls):
|
||||
def cost_values(cls) -> list[dict[str, str]]:
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_item_id
|
||||
if not ifc_id:
|
||||
@@ -275,14 +291,14 @@ class CostSchedulesData:
|
||||
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
|
||||
|
||||
@classmethod
|
||||
def quantity_types(cls):
|
||||
def quantity_types(cls) -> list[tuple[str, str, str]]:
|
||||
return [
|
||||
(t.name(), t.name(), "")
|
||||
for t in tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_cost_schedule_types(cls):
|
||||
def get_cost_schedule_types(cls) -> list[tuple[str, str, str]]:
|
||||
types = ifcopenshell.util.cost.get_cost_schedule_types(tool.Ifc.get())
|
||||
return [(t["name"], t["name"], t["description"]) for t in types]
|
||||
|
||||
@@ -299,7 +315,7 @@ class CostItemRatesData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def schedule_of_rates(cls):
|
||||
def schedule_of_rates(cls) -> list[tuple[str, str, str]]:
|
||||
return [
|
||||
(str(s.id()), s.Name or "Unnamed", "")
|
||||
for s in tool.Ifc.get().by_type("IfcCostSchedule")
|
||||
|
||||
@@ -24,6 +24,7 @@ import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.cost as core
|
||||
from pathlib import Path
|
||||
from typing import get_args, TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
@@ -267,7 +268,7 @@ class AssignCostItemQuantity(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Assign Cost Item Quantity"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
cost_item: bpy.props.IntProperty()
|
||||
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
|
||||
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
|
||||
)
|
||||
prop_name: bpy.props.StringProperty()
|
||||
@@ -551,7 +552,19 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
|
||||
cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
|
||||
core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.refresh_cost_schedule_csv"
|
||||
bl_label = "Refresh Cost Schedule CSV"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def _execute(self, context):
|
||||
core.refresh_cost_schedule_csv(tool.Ifc, tool.Cost)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -687,8 +700,9 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_cost_schedules"
|
||||
bl_label = "Export Cost Schedule"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
|
||||
cost_schedule: bpy.props.IntProperty()
|
||||
bl_description = "Export current/all cost schedules as CSV, XSLX or ODS files to the provided directory."
|
||||
|
||||
cost_schedule: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||
format: bpy.props.EnumProperty(name="Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
|
||||
directory: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_folder: bpy.props.BoolProperty(
|
||||
@@ -697,7 +711,15 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
cost_schedule: int
|
||||
format: Literal["CSV", "XLSX", "ODS"]
|
||||
directory: str
|
||||
|
||||
def check(self, context):
|
||||
if self.filepath != self.directory:
|
||||
self.filepath = self.directory
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
@@ -706,15 +728,12 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
def execute(self, context):
|
||||
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
|
||||
r = core.export_cost_schedules(
|
||||
tool.Cost, filepath=self.directory, format=self.format, cost_schedule=cost_schedule
|
||||
tool.Cost, dirpath=self.directory, format=self.format, cost_schedule=cost_schedule
|
||||
)
|
||||
if isinstance(r, str):
|
||||
self.report({"ERROR"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text="Choose a format")
|
||||
self.layout.prop(self, "format")
|
||||
@@ -726,7 +745,7 @@ class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Clear Cost Item Product Assignments"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
cost_item: bpy.props.IntProperty()
|
||||
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
|
||||
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
|
||||
)
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ def update_cost_item_identification(self: "CostItem", context: bpy.types.Context
|
||||
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}},
|
||||
)
|
||||
if props.active_cost_item_id == self.ifc_definition_id:
|
||||
attribute = props.cost_item_attributes.get("Identification")
|
||||
attribute = props.cost_item_attributes["Identification"]
|
||||
attribute.string_value = self.identification
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
|
||||
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
|
||||
)
|
||||
if props.active_cost_item_id == self.ifc_definition_id:
|
||||
attribute = props.cost_item_attributes.get("Name")
|
||||
attribute = props.cost_item_attributes["Name"]
|
||||
attribute.string_value = self.name
|
||||
|
||||
|
||||
@@ -167,6 +167,15 @@ class CostItemQuantity(PropertyGroup):
|
||||
total_cost_quantity: float
|
||||
|
||||
|
||||
class CostItemsMapping(PropertyGroup):
|
||||
cost_item_id: IntProperty(name="cost_item_id")
|
||||
csv_filepath: StringProperty(name="filepath")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
cost_item_id: int
|
||||
csv_filepath: str
|
||||
|
||||
|
||||
class CostItemType(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
@@ -272,6 +281,7 @@ class BIMCostProperties(PropertyGroup):
|
||||
custom_currency: StringProperty(
|
||||
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
|
||||
)
|
||||
cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostItemsMapping)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
cost_schedule_predefined_types: str
|
||||
@@ -326,3 +336,4 @@ class BIMCostProperties(PropertyGroup):
|
||||
show_cost_item_operators: bool
|
||||
currency: str
|
||||
custom_currency: str
|
||||
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostItemsMapping]
|
||||
|
||||
@@ -71,6 +71,26 @@ class BIM_PT_cost_schedules(Panel):
|
||||
text="Currently editing: {}[{}]".format(cost_schedule["name"], cost_schedule["predefined_type"]),
|
||||
icon="LINENUMBERS_ON",
|
||||
)
|
||||
|
||||
row0 = self.layout.row(align=True)
|
||||
col = row0.column()
|
||||
col.label(text="Linked CSV:")
|
||||
row_1 = col.row(align=True)
|
||||
if self.props.active_cost_schedule_id in [item.cost_item_id for item in self.props.cost_schedule_files]:
|
||||
file = next(
|
||||
(
|
||||
item.csv_filepath
|
||||
for item in self.props.cost_schedule_files
|
||||
if item.cost_item_id == self.props.active_cost_schedule_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
row_1.label(text=file)
|
||||
row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="")
|
||||
else:
|
||||
row_1.label(text="No CSV file found")
|
||||
row_1.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="")
|
||||
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
col = grid.column()
|
||||
row1 = col.row(align=True)
|
||||
@@ -364,8 +384,29 @@ class BIM_PT_cost_item_types(Panel):
|
||||
# TODO
|
||||
col = grid.column()
|
||||
|
||||
has_quantity_names = CostProp.get_resource_quantity_names(self, context)
|
||||
|
||||
row2 = col.row(align=True)
|
||||
row2.label(text="Resources")
|
||||
# row2.label(text="Resources")
|
||||
total_cost_item_resources = len(self.props.cost_item_resources)
|
||||
row2.label(text="Resources({})".format(total_cost_item_resources))
|
||||
|
||||
op = row2.operator("bim.calculate_cost_item_resource_value", text="", icon="DISC")
|
||||
op.cost_item = cost_item.ifc_definition_id
|
||||
|
||||
rtprops = context.scene.BIMResourceTreeProperties
|
||||
rprops = context.scene.BIMResourceProperties
|
||||
if rtprops.resources and rprops.active_resource_index < len(rtprops.resources):
|
||||
if has_quantity_names:
|
||||
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
|
||||
op.related_object_type = "RESOURCE"
|
||||
op.cost_item = cost_item.ifc_definition_id
|
||||
op.prop_name = self.props.resource_quantity_names
|
||||
|
||||
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="ADD")
|
||||
op.related_object_type = "RESOURCE"
|
||||
op.cost_item = cost_item.ifc_definition_id
|
||||
op.prop_name = ""
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
|
||||
@@ -132,13 +132,14 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_document"
|
||||
bl_label = "Assign Document"
|
||||
bl_description = "Assign active document to the selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
document = tool.Ifc.get().by_id(self.document)
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
|
||||
objs = [bpy.data.objects[self.obj]] if self.obj else tool.Blender.get_selected_objects()
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
|
||||
@@ -30,7 +30,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
|
||||
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
|
||||
@@ -79,3 +79,7 @@ class BIMDocumentProperties(PropertyGroup):
|
||||
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
active_document_index: int
|
||||
is_editing: bool
|
||||
|
||||
@property
|
||||
def active_document(self) -> Union[Document, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.documents, self.active_document_index)
|
||||
|
||||
@@ -61,14 +61,17 @@ class BIM_PT_documents(Panel):
|
||||
if self.props.breadcrumbs:
|
||||
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
|
||||
|
||||
active_document = self.props.active_document
|
||||
|
||||
if self.props.active_document_id:
|
||||
row.operator("bim.edit_document", text="", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
|
||||
elif self.props.documents and self.props.active_document_index < len(self.props.documents):
|
||||
ifc_definition_id = self.props.documents[self.props.active_document_index].ifc_definition_id
|
||||
elif active_document:
|
||||
ifc_definition_id = active_document.ifc_definition_id
|
||||
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
|
||||
ifc_definition_id
|
||||
)
|
||||
row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id
|
||||
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
|
||||
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ classes = (
|
||||
operator.AddAnnotationType,
|
||||
operator.AddDrawing,
|
||||
operator.AddDrawingStyle,
|
||||
operator.AddDrawingStyleAttribute,
|
||||
operator.AddDrawingToSheet,
|
||||
operator.AddReference,
|
||||
operator.AddReferenceImage,
|
||||
@@ -80,12 +79,10 @@ classes = (
|
||||
operator.RemoveDrawing,
|
||||
operator.RemoveDrawingFromSheet,
|
||||
operator.RemoveDrawingStyle,
|
||||
operator.RemoveDrawingStyleAttribute,
|
||||
operator.RemoveReference,
|
||||
operator.RemoveSchedule,
|
||||
operator.RemoveSheet,
|
||||
operator.RemoveTextLiteral,
|
||||
operator.ResizeText,
|
||||
operator.SaveDrawingStyle,
|
||||
operator.SaveDrawingStylesData,
|
||||
operator.SelectAllDrawings,
|
||||
|
||||
@@ -29,52 +29,6 @@ from typing import Optional
|
||||
|
||||
|
||||
class Annotator:
|
||||
@staticmethod
|
||||
def add_text(related_element: Optional[ifcopenshell.entity_instance] = None) -> bpy.types.Object:
|
||||
curve = bpy.data.curves.new(type="FONT", name="Text")
|
||||
curve.body = "TEXT"
|
||||
obj = bpy.data.objects.new("Text", curve)
|
||||
obj.matrix_world = bpy.context.scene.camera.matrix_world
|
||||
if related_element is None:
|
||||
location, _, _, _ = Annotator.get_placeholder_coords(bpy.context)
|
||||
else:
|
||||
curve.BIMAssignedProductProperties.related_element = related_element
|
||||
location = related_element.location
|
||||
obj.location = location
|
||||
obj.hide_render = True
|
||||
font = bpy.data.fonts.get("OpenGost TypeB TT")
|
||||
if not font:
|
||||
font = bpy.data.fonts.load(
|
||||
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
|
||||
)
|
||||
font.name = "OpenGost Type B TT"
|
||||
curve.font = font
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.font_size = "2.5"
|
||||
collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection
|
||||
collection.objects.link(obj)
|
||||
Annotator.resize_text(obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def resize_text(text_obj: bpy.types.Object) -> None:
|
||||
camera = None
|
||||
group = tool.Drawing.get_drawing_group(tool.Ifc.get_entity(text_obj))
|
||||
for element in tool.Drawing.get_drawing_elements(group):
|
||||
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
camera = tool.Ifc.get_object(element)
|
||||
break
|
||||
if not camera:
|
||||
return
|
||||
# This is a magic number for OpenGost
|
||||
font_size = 1.6 / 1000
|
||||
props = tool.Drawing.get_text_props(text_obj)
|
||||
font_size *= float(props.font_size)
|
||||
|
||||
font_size /= tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
|
||||
|
||||
text_obj.data.size = font_size
|
||||
|
||||
@staticmethod
|
||||
def add_line_to_annotation(
|
||||
obj: bpy.types.Object, co1: Optional[Vector] = None, co2: Optional[Vector] = None
|
||||
@@ -129,8 +83,14 @@ class Annotator:
|
||||
def get_annotation_obj(
|
||||
drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE
|
||||
) -> bpy.types.Object:
|
||||
assert bpy.context.scene
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
|
||||
assert isinstance(camera, bpy.types.Object)
|
||||
# those annotations you want to obey the depth of the 3d cursor
|
||||
if object_type == "PLAN_LEVEL":
|
||||
co1 = bpy.context.scene.cursor.location.copy()
|
||||
else:
|
||||
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
|
||||
matrix_world = tool.Drawing.get_camera_matrix(camera)
|
||||
matrix_world.translation = co1
|
||||
collection = tool.Blender.get_object_bim_props(camera).collection
|
||||
|
||||
@@ -34,9 +34,7 @@ def refresh():
|
||||
DrawingsData.is_loaded = False
|
||||
ElementFiltersData.is_loaded = False
|
||||
AnnotationData.is_loaded = False
|
||||
DecoratorData.data = {}
|
||||
DecoratorData.cut_cache = {}
|
||||
DecoratorData.layerset_cache = {}
|
||||
DecoratorData.is_loaded = False
|
||||
|
||||
|
||||
class ProductAssignmentsData:
|
||||
@@ -120,7 +118,7 @@ class DrawingsData:
|
||||
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
|
||||
|
||||
@classmethod
|
||||
def location_hint(cls):
|
||||
def location_hint(cls) -> list[tuple[tool.Drawing.LocationHintType, str, str]]:
|
||||
props = tool.Drawing.get_document_props()
|
||||
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
|
||||
results = [("0", "Origin", "")]
|
||||
@@ -134,12 +132,10 @@ class DrawingsData:
|
||||
|
||||
@classmethod
|
||||
def active_drawing_pset_data(cls):
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Drawing.get_document_props()
|
||||
drawing_id = props.active_drawing_id
|
||||
if drawing_id == 0:
|
||||
drawing = props.get_active_drawing()
|
||||
if drawing is None:
|
||||
return {}
|
||||
drawing = ifc_file.by_id(drawing_id)
|
||||
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
||||
|
||||
|
||||
@@ -239,6 +235,41 @@ class DecoratorData:
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
|
||||
@classmethod
|
||||
def load(cls, handler):
|
||||
cls.is_loaded = True
|
||||
cls.cut_cache = {}
|
||||
cls.layerset_cache = {}
|
||||
|
||||
text = {}
|
||||
dimension = {}
|
||||
fall = {}
|
||||
symbol = {}
|
||||
for obj in bpy.context.visible_objects:
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
continue
|
||||
if tool.Drawing.is_annotation_object_type(element, ("TEXT", "TEXT_LEADER")):
|
||||
text[obj.name] = cls.get_text_data(obj)
|
||||
if text[obj.name]["Symbol"]:
|
||||
symbol[obj.name] = cls.get_symbol_data(obj)
|
||||
elif tool.Drawing.is_annotation_object_type(
|
||||
element, ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
|
||||
):
|
||||
dimension[obj.name] = cls.get_dimension_data(obj)
|
||||
elif tool.Drawing.is_annotation_object_type(
|
||||
element, ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT")
|
||||
):
|
||||
fall[obj.name] = cls.get_fall_data(obj)
|
||||
elif tool.Drawing.is_annotation_object_type(element, ("SYMBOL",)):
|
||||
symbol[obj.name] = cls.get_symbol_data(obj)
|
||||
cls.data = {
|
||||
"text": text,
|
||||
"dimension": dimension,
|
||||
"fall": fall,
|
||||
"symbol": symbol,
|
||||
"object_decorators": cls.object_decorators(handler),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_batting_thickness(cls, obj):
|
||||
"""used by IfcAnnotations with ObjectType = "BATTING" """
|
||||
@@ -296,17 +327,10 @@ class DecoratorData:
|
||||
return display_data
|
||||
|
||||
@classmethod
|
||||
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
||||
def get_text_data(cls, obj: bpy.types.Object) -> dict:
|
||||
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
|
||||
returns font size in mm for current ifc text object"""
|
||||
result = cls.data.get(obj.name, None)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
return None
|
||||
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
# getting font size
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||
@@ -344,14 +368,7 @@ class DecoratorData:
|
||||
|
||||
literals_data.append(literal_data)
|
||||
|
||||
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
|
||||
cls.data[obj.name] = text_data
|
||||
return text_data
|
||||
|
||||
@classmethod
|
||||
def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]:
|
||||
"""used by IfcAnnotations with ObjectType MULTI_SYMBOL"""
|
||||
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
|
||||
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
|
||||
|
||||
@classmethod
|
||||
def get_dimension_data(cls, obj):
|
||||
@@ -359,19 +376,7 @@ class DecoratorData:
|
||||
|
||||
DIMENSION / DIAMETER / SECTION_LEVEL / PLAN_LEVEL / RADIUS
|
||||
"""
|
||||
result = cls.data.get(obj.name, None)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
|
||||
if (
|
||||
not element
|
||||
or not element.is_a("IfcAnnotation")
|
||||
or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types
|
||||
):
|
||||
return None
|
||||
|
||||
dimension_style = "arrow"
|
||||
fill_bg = False
|
||||
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
||||
@@ -390,7 +395,7 @@ class DecoratorData:
|
||||
custom_unit_list = pset_data.get("CustomUnit", None) or ""
|
||||
custom_unit = custom_unit_list[0] if custom_unit_list else ""
|
||||
|
||||
dimension_data = {
|
||||
return {
|
||||
"dimension_style": dimension_style,
|
||||
"show_description_only": show_description_only,
|
||||
"suppress_zero_inches": suppress_zero_inches,
|
||||
@@ -399,8 +404,59 @@ class DecoratorData:
|
||||
"fill_bg": fill_bg,
|
||||
"custom_unit": custom_unit,
|
||||
}
|
||||
cls.data[obj.name] = dimension_data
|
||||
return dimension_data
|
||||
|
||||
@classmethod
|
||||
def get_fall_data(cls, obj):
|
||||
object_type = None
|
||||
if element := tool.Ifc.get_entity(obj):
|
||||
object_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
return {"object_type": object_type}
|
||||
|
||||
@classmethod
|
||||
def get_symbol_data(cls, obj):
|
||||
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
|
||||
|
||||
@classmethod
|
||||
def object_decorators(cls, handler):
|
||||
import bonsai.bim.module.drawing.decoration
|
||||
|
||||
if not bonsai.bim.module.drawing.decoration.DecorationsHandler.installed:
|
||||
return []
|
||||
|
||||
props = tool.Drawing.get_document_props()
|
||||
if (drawing := props.get_active_drawing()) is None:
|
||||
return []
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
assert isinstance(camera, bpy.types.Object)
|
||||
collection = tool.Blender.get_object_bim_props(camera).collection
|
||||
assert collection
|
||||
|
||||
results = []
|
||||
viewport = tool.Blender.get_view3d_space()
|
||||
|
||||
for obj in collection.all_objects:
|
||||
if not obj.visible_get(viewport=viewport):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if not element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if object_type == "DRAWING":
|
||||
continue
|
||||
if dec := handler.decorators.get(object_type, None):
|
||||
results.append((obj, dec))
|
||||
elif isinstance(obj.data, bpy.types.Mesh):
|
||||
if object_type == "LINEWORK" and "dashed" in str(
|
||||
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
||||
).split(" "):
|
||||
results.append((obj, handler.decorators["HIDDEN_LINE"]))
|
||||
else:
|
||||
results.append((obj, handler.decorators["MISC"]))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
class AnnotationData:
|
||||
|
||||
@@ -562,8 +562,7 @@ class BaseDecorator:
|
||||
MiscDecorator.decorate(self, context, obj)
|
||||
return
|
||||
|
||||
symbol = DecoratorData.get_symbol(obj)
|
||||
if not symbol:
|
||||
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
|
||||
return
|
||||
|
||||
for vert in mesh.vertices:
|
||||
@@ -572,8 +571,7 @@ class BaseDecorator:
|
||||
return
|
||||
|
||||
# EMPTY objects
|
||||
symbol = DecoratorData.get_symbol(obj)
|
||||
if not symbol:
|
||||
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
|
||||
return
|
||||
|
||||
rotation = -Vector((1, 0)).angle_signed(annotation_dir)
|
||||
@@ -590,7 +588,7 @@ class BaseDecorator:
|
||||
"""if `text_world_position` is not provided, the object's location will be used"""
|
||||
|
||||
if not text_world_position:
|
||||
text_world_position = obj.location
|
||||
text_world_position = obj.matrix_world.translation
|
||||
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
@@ -599,7 +597,7 @@ class BaseDecorator:
|
||||
if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)):
|
||||
return
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
text_data = DecoratorData.get_ifc_text_data(obj)
|
||||
text_data = DecoratorData.data["text"].get(obj.name, None)
|
||||
if props.is_editing:
|
||||
text_data = text_data | props.get_text_edited_data()
|
||||
literals_data = text_data["Literals"]
|
||||
@@ -656,7 +654,10 @@ class DimensionDecorator(BaseDecorator):
|
||||
viewportDrawingScale = self.get_viewport_drawing_scale(context)
|
||||
|
||||
# setup geometry parameters
|
||||
dimension_style = DecoratorData.get_dimension_data(obj)["dimension_style"]
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
dimension_style = dimension_data["dimension_style"]
|
||||
if dimension_style == "oblique":
|
||||
size = viewportDrawingScale * 10 # OLBIQUE_SYMBOL_SIZE
|
||||
angle = radians(45)
|
||||
@@ -718,7 +719,9 @@ class DimensionDecorator(BaseDecorator):
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
description = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
show_description_only = dimension_data["show_description_only"]
|
||||
text_prefix = dimension_data["text_prefix"]
|
||||
text_suffix = dimension_data["text_suffix"]
|
||||
@@ -952,7 +955,9 @@ class RadiusDecorator(BaseDecorator):
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
description = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
viewportDrawingScale = self.get_viewport_drawing_scale(context)
|
||||
text_offset = 20 * viewportDrawingScale
|
||||
|
||||
@@ -991,8 +996,6 @@ class FallDecorator(BaseDecorator):
|
||||
# generate label text
|
||||
# same function as in svgwriter.py
|
||||
def get_label_text():
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
B, A = [v.co.xyz for v in spline_points[:2]]
|
||||
rise = abs(A.z - B.z)
|
||||
O = A.copy()
|
||||
@@ -1004,8 +1007,8 @@ class FallDecorator(BaseDecorator):
|
||||
else:
|
||||
angle = 90
|
||||
|
||||
# ues SLOPE_ANGLE as default
|
||||
object_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
# uses SLOPE_ANGLE as default
|
||||
object_type = DecoratorData.data["fall"].get(obj, {}).get("object_type", None)
|
||||
if object_type in ("FALL", "SLOPE_ANGLE"):
|
||||
return f"{angle}°"
|
||||
elif object_type == "SLOPE_FRACTION":
|
||||
@@ -1016,6 +1019,7 @@ class FallDecorator(BaseDecorator):
|
||||
if angle == 90:
|
||||
return "-"
|
||||
return f"{round(angle_tg * 100)} %"
|
||||
return "NO DATA"
|
||||
|
||||
if spline_points:
|
||||
text = get_label_text()
|
||||
@@ -1179,7 +1183,9 @@ class PlanLevelDecorator(BaseDecorator):
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
description = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
|
||||
for verts in splines:
|
||||
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
|
||||
@@ -1262,7 +1268,9 @@ class SectionLevelDecorator(BaseDecorator):
|
||||
storey = tool.Drawing.get_annotation_element(element)
|
||||
tag = storey.Name if storey else element.Description
|
||||
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
|
||||
for verts in splines:
|
||||
|
||||
@@ -1639,9 +1647,15 @@ class CutDecorator:
|
||||
all_vertex_i_offset = 0
|
||||
selected_vertex_i_offset = 0
|
||||
|
||||
classes_no_cut_str = tool.Drawing.get_document_props().classes_no_cut
|
||||
classes_no_cut = [word.strip() for word in classes_no_cut_str.split(",")]
|
||||
|
||||
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
continue
|
||||
# Skip the elements in the following classes
|
||||
if element.is_a() in classes_no_cut:
|
||||
continue
|
||||
self.decorate(context, obj, element)
|
||||
|
||||
verts, edges = DecoratorData.cut_cache[element.id()]
|
||||
@@ -1721,11 +1735,11 @@ class CutDecorator:
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
else:
|
||||
if tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
|
||||
DecoratorData.cut_cache[element.id()] = (verts, edges)
|
||||
else:
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
|
||||
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
element_id = element.id()
|
||||
@@ -1904,6 +1918,7 @@ class DecorationsHandler:
|
||||
]
|
||||
|
||||
installed = None
|
||||
handler = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
@@ -1913,6 +1928,8 @@ class DecorationsHandler:
|
||||
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
|
||||
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
||||
if not DecoratorData.is_loaded:
|
||||
DecoratorData.load(handler)
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
@@ -1935,47 +1952,9 @@ class DecorationsHandler:
|
||||
for decorator in self.decorators.values():
|
||||
decorator.font_id = font_id
|
||||
|
||||
def get_objects_and_decorators(self, collection):
|
||||
# TODO: do it in data instead of the handler for performance?
|
||||
results = []
|
||||
viewport = bpy.context.space_data
|
||||
|
||||
for obj in collection.all_objects:
|
||||
if not obj.visible_get(viewport=viewport):
|
||||
continue
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
|
||||
if not element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
|
||||
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if object_type == "DRAWING":
|
||||
continue
|
||||
|
||||
if dec := self.decorators.get(object_type, None):
|
||||
results.append((obj, dec))
|
||||
|
||||
elif isinstance(obj.data, bpy.types.Mesh):
|
||||
if object_type == "LINEWORK" and "dashed" in str(
|
||||
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
||||
).split(" "):
|
||||
results.append((obj, self.decorators["HIDDEN_LINE"]))
|
||||
else:
|
||||
results.append((obj, self.decorators["MISC"]))
|
||||
|
||||
return results
|
||||
|
||||
def __call__(self, context):
|
||||
collection, _ = helper.get_active_drawing(context.scene)
|
||||
if collection is None:
|
||||
return
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
object_decorators = self.get_objects_and_decorators(collection)
|
||||
for obj, decorator in object_decorators:
|
||||
for obj, decorator in DecoratorData.data["object_decorators"]:
|
||||
decorator.decorate(context, obj)
|
||||
|
||||
@@ -37,29 +37,23 @@ def depsgraph_update_pre_handler(scene):
|
||||
|
||||
|
||||
def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
|
||||
"""Sync scene render resolution with the active drawing
|
||||
and prevent user from manually changing ``ortho_scale`` on IFC camera."""
|
||||
props = tool.Drawing.get_document_props()
|
||||
if not scene.camera or "/" not in scene.camera.name or not props.drawings:
|
||||
camera_obj = scene.camera
|
||||
if not camera_obj or "/" not in camera_obj.name or not props.drawings:
|
||||
return
|
||||
assert isinstance(scene.camera.data, bpy.types.Camera)
|
||||
props = scene.camera.data.BIMCameraProperties
|
||||
ortho_scale = max((props.width, props.height))
|
||||
aspect_ratio = props.width / props.height
|
||||
if (scene.camera.data.ortho_scale != ortho_scale) or (
|
||||
scene.render.resolution_x / scene.render.resolution_y != aspect_ratio
|
||||
assert isinstance((camera := camera_obj.data), bpy.types.Camera)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
|
||||
if camera.type != props.camera_type:
|
||||
camera.type = props.camera_type
|
||||
|
||||
ortho_scale, aspect_ratio = props.get_scale_and_aspect_ratio()
|
||||
scene_render = scene.render
|
||||
if (camera.ortho_scale != ortho_scale) or not tool.Cad.is_x(
|
||||
scene_render.resolution_x / scene_render.resolution_y, aspect_ratio
|
||||
):
|
||||
scene.camera.data.ortho_scale = ortho_scale
|
||||
|
||||
diagram_scale = tool.Drawing.get_diagram_scale(scene.camera)
|
||||
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
|
||||
|
||||
if props.width > props.height:
|
||||
aspect_ratio = props.height / props.width
|
||||
raster_x = ortho_scale * scale_ratio * props.dpi / 0.0254
|
||||
raster_y = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
|
||||
else:
|
||||
aspect_ratio = props.width / props.height
|
||||
raster_x = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
|
||||
raster_y = ortho_scale * scale_ratio * props.dpi / 0.0254
|
||||
|
||||
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
|
||||
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
|
||||
raster_x, raster_y = props.update_camera_resolution()
|
||||
scene_render.resolution_x = raster_x
|
||||
scene_render.resolution_y = raster_y
|
||||
|
||||
@@ -305,7 +305,7 @@ def format_distance(
|
||||
if unit_length == "MILLIMETERS":
|
||||
value = value / 1000
|
||||
|
||||
if precision:
|
||||
if precision and isinstance(precision, float):
|
||||
value = precision * round(float(value) / precision)
|
||||
|
||||
if decimal_places is not None:
|
||||
@@ -392,18 +392,6 @@ def format_distance(
|
||||
return tx_dist
|
||||
|
||||
|
||||
def get_active_drawing(
|
||||
scene: bpy.types.Scene,
|
||||
) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]:
|
||||
"""Get active drawing collection and camera"""
|
||||
props = tool.Drawing.get_document_props()
|
||||
try:
|
||||
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
|
||||
return tool.Blender.get_object_bim_props(camera).collection, camera
|
||||
except:
|
||||
return None, None
|
||||
|
||||
|
||||
def get_project_collection(scene):
|
||||
"""Get main project collection"""
|
||||
|
||||
@@ -413,13 +401,15 @@ def get_project_collection(scene):
|
||||
return colls[0]
|
||||
|
||||
|
||||
def parse_diagram_scale(camera):
|
||||
def parse_diagram_scale(camera: bpy.types.Camera) -> float:
|
||||
"""Returns numeric value of scale"""
|
||||
if camera.BIMCameraProperties.diagram_scale == "CUSTOM":
|
||||
_, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|")
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
if props.diagram_scale == "CUSTOM":
|
||||
numerator = props.custom_scale_numerator
|
||||
denominator = props.custom_scale_denominator
|
||||
else:
|
||||
_, fraction = camera.BIMCameraProperties.diagram_scale.split("|")
|
||||
numerator, denominator = fraction.split("/")
|
||||
_, fraction = props.diagram_scale.split("|")
|
||||
numerator, denominator = fraction.split("/")
|
||||
return float(numerator) / float(denominator)
|
||||
|
||||
|
||||
@@ -431,12 +421,11 @@ def ortho_view_frame(
|
||||
Similar to `bpy.types.Camera.view_frame`
|
||||
|
||||
:arg camera: camera of drawing
|
||||
:type camera: bpy.types.Camera + BIMCameraProperties
|
||||
:arg margin: margins, in scene units
|
||||
:type margin: float
|
||||
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
|
||||
"""
|
||||
aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
aspect = props.raster_y / props.raster_x
|
||||
size = camera.ortho_scale
|
||||
hwidth = size * 0.5
|
||||
hheight = size * 0.5 * aspect
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,11 +22,13 @@ import json
|
||||
import enum
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.drawing as core
|
||||
import bonsai.bim.module.drawing.annotation as annotation
|
||||
import bonsai.bim.module.drawing.decoration as decoration
|
||||
from mathutils import Matrix
|
||||
from bonsai.bim.prop import BIMFilterGroup
|
||||
from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
|
||||
from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
|
||||
@@ -44,7 +46,7 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
BoolVectorProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal, Any
|
||||
from typing import TYPE_CHECKING, Literal, Any, Callable, get_args, Union
|
||||
|
||||
|
||||
diagram_scales_enum = []
|
||||
@@ -55,23 +57,23 @@ def purge():
|
||||
diagram_scales_enum = []
|
||||
|
||||
|
||||
def update_target_view(self, context):
|
||||
def update_target_view_doc(self: "DocProperties", context: bpy.types.Context) -> None:
|
||||
DrawingsData.data["location_hint"] = DrawingsData.location_hint()
|
||||
|
||||
|
||||
def get_location_hint(self, context):
|
||||
def get_location_hint(self: "DocProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
return DrawingsData.data["location_hint"]
|
||||
|
||||
|
||||
def update_diagram_scale(self, context):
|
||||
def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if not self.update_props:
|
||||
return
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
assert context.scene
|
||||
if not (camera := context.scene.camera) or camera.data != self.id_data:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.scene.camera)
|
||||
if not element:
|
||||
if not (element := tool.Ifc.get_entity(camera)):
|
||||
return
|
||||
try:
|
||||
element = (
|
||||
@@ -89,13 +91,15 @@ def update_diagram_scale(self, context):
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale)
|
||||
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing")
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=diagram_scale)
|
||||
self.update_camera_resolution()
|
||||
|
||||
|
||||
def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if not self.update_props:
|
||||
return
|
||||
assert context.scene
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.scene.camera)
|
||||
@@ -118,8 +122,9 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts})
|
||||
|
||||
|
||||
def get_diagram_scales(self, context):
|
||||
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
global diagram_scales_enum
|
||||
assert context.scene
|
||||
if (
|
||||
len(diagram_scales_enum) < 1
|
||||
or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
|
||||
@@ -200,44 +205,39 @@ def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None:
|
||||
bpy.ops.bim.save_drawing_styles_data(rename_style=True, rename_style_from=old_value, rename_style_to=new_value)
|
||||
|
||||
|
||||
def update_document_name(self, context):
|
||||
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
|
||||
document = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
core.update_document_name(tool.Ifc, tool.Drawing, document=document, name=self.name)
|
||||
|
||||
|
||||
def update_has_underlay(self, context):
|
||||
def update_has_underlay(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if not self.update_props:
|
||||
return
|
||||
update_layer(self, context, "HasUnderlay", self.has_underlay)
|
||||
assert context.scene
|
||||
# making sure that camera is active
|
||||
if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data):
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
|
||||
|
||||
def update_has_linework(self, context):
|
||||
update_layer(self, context, "HasLinework", self.has_linework)
|
||||
def get_update_layer_callback(
|
||||
camera_prop_name: str, pset_prop_name: str
|
||||
) -> Callable[["BIMCameraProperties", bpy.types.Context], None]:
|
||||
def update_layer_callback(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
update_layer(self, context, pset_prop_name, getattr(self, camera_prop_name))
|
||||
|
||||
return update_layer_callback
|
||||
|
||||
|
||||
def update_has_annotation(self, context):
|
||||
update_layer(self, context, "HasAnnotation", self.has_annotation)
|
||||
def update_target_view_camera(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if self.update_props and self.target_view != "MODEL_VIEW":
|
||||
self.camera_type = "ORTHO"
|
||||
update_layer(self, context, "TargetView", self.target_view)
|
||||
|
||||
|
||||
def update_dpi(self, context):
|
||||
update_layer(self, context, "DPI", self.dpi)
|
||||
|
||||
|
||||
def update_linework_mode(self, context):
|
||||
update_layer(self, context, "LineworkMode", self.linework_mode)
|
||||
|
||||
|
||||
def update_fill_mode(self, context):
|
||||
update_layer(self, context, "FillMode", self.fill_mode)
|
||||
|
||||
|
||||
def update_cut_mode(self, context):
|
||||
update_layer(self, context, "CutMode", self.cut_mode)
|
||||
|
||||
|
||||
def update_layer(self, context, name, value):
|
||||
def update_layer(self: "BIMCameraProperties", context: bpy.types.Context, name: str, value: Any) -> None:
|
||||
assert context.scene
|
||||
if not self.update_props:
|
||||
return
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
@@ -287,10 +287,24 @@ class Variable(PropertyGroup):
|
||||
prop_key: StringProperty(name="Property Key")
|
||||
|
||||
|
||||
TargetView = Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
|
||||
TARGET_VIEW_ITEMS: list[tuple[TargetView, str, str]] = [
|
||||
("PLAN_VIEW", "Plan", ""),
|
||||
("ELEVATION_VIEW", "Elevation", ""),
|
||||
("SECTION_VIEW", "Section", ""),
|
||||
("REFLECTED_PLAN_VIEW", "RCP", ""),
|
||||
("MODEL_VIEW", "Model", ""),
|
||||
]
|
||||
|
||||
|
||||
class Drawing(PropertyGroup):
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
name: StringProperty(name="Name", update=update_drawing_name)
|
||||
target_view: StringProperty(name="Target View")
|
||||
target_view: EnumProperty(
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
items=TARGET_VIEW_ITEMS,
|
||||
)
|
||||
is_selected: BoolProperty(name="Is Selected", default=True)
|
||||
is_drawing: BoolProperty(name="Is Drawing", default=False)
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=True)
|
||||
@@ -298,7 +312,7 @@ class Drawing(PropertyGroup):
|
||||
if TYPE_CHECKING:
|
||||
ifc_definition_id: int
|
||||
name: str
|
||||
target_view: str
|
||||
target_view: TargetView
|
||||
is_selected: bool
|
||||
is_drawing: bool
|
||||
is_expanded: bool
|
||||
@@ -334,21 +348,34 @@ class Sheet(PropertyGroup):
|
||||
is_expanded: bool
|
||||
|
||||
|
||||
RenderType = Literal["DEFAULT", "VIEWPORT"]
|
||||
RENDER_TYPE_ENUM_ITEMS: dict[RenderType, tuple[str, str]] = {
|
||||
"DEFAULT": ("Default", "Use default Blender render."),
|
||||
"VIEWPORT": ("Viewport", "Render active viewport (using 'Rendered' viewport shading type)."),
|
||||
}
|
||||
|
||||
|
||||
class DrawingStyle(PropertyGroup):
|
||||
name: StringProperty(name="Name", get=get_drawing_style_name, set=set_drawing_style_name)
|
||||
raster_style: StringProperty(name="Raster Style", default="{}")
|
||||
raster_style: StringProperty(
|
||||
name="Raster Style",
|
||||
description="JSON string for drawing style settings.",
|
||||
default="{}",
|
||||
)
|
||||
render_type: EnumProperty(
|
||||
items=[
|
||||
("NONE", "None", ""),
|
||||
("DEFAULT", "Default", ""),
|
||||
("VIEWPORT", "Viewport", ""),
|
||||
],
|
||||
items=[(k, *v) for k, v in RENDER_TYPE_ENUM_ITEMS.items()],
|
||||
name="Render Type",
|
||||
default="VIEWPORT",
|
||||
)
|
||||
include_query: StringProperty(name="Include Query")
|
||||
exclude_query: StringProperty(name="Exclude Query")
|
||||
attributes: CollectionProperty(name="Attributes", type=StrProperty)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
raster_style: str
|
||||
render_type: RenderType
|
||||
include_query: str
|
||||
exclude_query: str
|
||||
|
||||
|
||||
class RasterStyleProperty(enum.Enum):
|
||||
@@ -375,16 +402,10 @@ class DocProperties(PropertyGroup):
|
||||
is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
|
||||
is_editing_references: BoolProperty(name="Is Editing References", default=False)
|
||||
target_view: EnumProperty(
|
||||
items=[
|
||||
("PLAN_VIEW", "Plan", ""),
|
||||
("ELEVATION_VIEW", "Elevation", ""),
|
||||
("SECTION_VIEW", "Section", ""),
|
||||
("REFLECTED_PLAN_VIEW", "RCP", ""),
|
||||
("MODEL_VIEW", "Model", ""),
|
||||
],
|
||||
items=TARGET_VIEW_ITEMS,
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
update=update_target_view,
|
||||
update=update_target_view_doc,
|
||||
)
|
||||
location_hint: EnumProperty(items=get_location_hint, name="Location Hint")
|
||||
drawings: CollectionProperty(name="Drawings", type=Drawing)
|
||||
@@ -430,6 +451,11 @@ class DocProperties(PropertyGroup):
|
||||
name="Classes to Wireframe",
|
||||
description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace",
|
||||
)
|
||||
classes_no_cut: StringProperty(
|
||||
default="IfcVirtualElement, IfcSpace",
|
||||
name="Classes that are not cut",
|
||||
description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
should_use_underlay_cache: bool
|
||||
@@ -439,7 +465,7 @@ class DocProperties(PropertyGroup):
|
||||
is_editing_schedules: bool
|
||||
is_editing_references: bool
|
||||
target_view: Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
|
||||
location_hint: str
|
||||
location_hint: tool.Drawing.LocationHintType
|
||||
drawings: bpy.types.bpy_prop_collection_idprop[Drawing]
|
||||
active_drawing_id: int
|
||||
active_drawing_index: int
|
||||
@@ -471,6 +497,29 @@ class DocProperties(PropertyGroup):
|
||||
imperial_precision: str
|
||||
tolerance: float
|
||||
classes_to_wireframe: str
|
||||
classes_no_cut: str
|
||||
|
||||
def get_active_drawing(self) -> Union[ifcopenshell.entity_instance, None]:
|
||||
drawing_id = self.active_drawing_id
|
||||
if drawing_id == 0:
|
||||
return None
|
||||
return tool.Ifc.get().by_id(drawing_id)
|
||||
|
||||
|
||||
def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
self.update_camera_resolution()
|
||||
|
||||
|
||||
def update_camera_type(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
assert isinstance(camera := self.id_data, bpy.types.Camera)
|
||||
camera.type = self.camera_type
|
||||
|
||||
|
||||
CameraType = Literal["PERSP", "ORTHO"]
|
||||
CAMERA_TYPE_ENUM_ITEMS: dict[CameraType, tuple[str, str]] = {
|
||||
"ORTHO": ("Orthographic", "Most common camera for the drawings, supporting all of the features."),
|
||||
"PERSP": ("Perspective", "The only avilable features for perspective camera: freestyle linework, underlay."),
|
||||
}
|
||||
|
||||
|
||||
class BIMCameraProperties(PropertyGroup):
|
||||
@@ -481,7 +530,7 @@ class BIMCameraProperties(PropertyGroup):
|
||||
],
|
||||
default="OPENCASCADE",
|
||||
name="Linework Mode",
|
||||
update=update_linework_mode,
|
||||
update=get_update_layer_callback("linework_mode", "LineworkMode"),
|
||||
)
|
||||
fill_mode: EnumProperty(
|
||||
items=[
|
||||
@@ -491,7 +540,7 @@ class BIMCameraProperties(PropertyGroup):
|
||||
],
|
||||
default="NONE",
|
||||
name="Fill Mode",
|
||||
update=update_fill_mode,
|
||||
update=get_update_layer_callback("fill_mode", "FillMode"),
|
||||
)
|
||||
cut_mode: EnumProperty(
|
||||
items=[
|
||||
@@ -500,11 +549,32 @@ class BIMCameraProperties(PropertyGroup):
|
||||
],
|
||||
default="BISECT",
|
||||
name="Cut Mode",
|
||||
update=update_cut_mode,
|
||||
update=get_update_layer_callback("cut_mode", "CutMode"),
|
||||
)
|
||||
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
||||
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
||||
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
||||
|
||||
# EPset_Drawing.
|
||||
has_underlay: BoolProperty(
|
||||
name="Underlay",
|
||||
default=False,
|
||||
update=update_has_underlay,
|
||||
)
|
||||
has_linework: BoolProperty(
|
||||
name="Linework",
|
||||
default=True,
|
||||
update=get_update_layer_callback("has_linework", "HasLinework"),
|
||||
)
|
||||
has_annotation: BoolProperty(
|
||||
name="Annotation",
|
||||
default=True,
|
||||
update=get_update_layer_callback("has_annotation", "HasAnnotation"),
|
||||
)
|
||||
target_view: EnumProperty(
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
items=TARGET_VIEW_ITEMS,
|
||||
update=update_target_view_camera,
|
||||
)
|
||||
|
||||
representation: StringProperty(name="Representation")
|
||||
view_name: StringProperty(name="View Name")
|
||||
diagram_scale: EnumProperty(items=get_diagram_scales, name="Drawing Scale", update=update_diagram_scale)
|
||||
@@ -512,26 +582,85 @@ class BIMCameraProperties(PropertyGroup):
|
||||
custom_scale_denominator: bpy.props.StringProperty(default="100", update=update_diagram_scale)
|
||||
raster_x: IntProperty(name="Raster X", default=1000)
|
||||
raster_y: IntProperty(name="Raster Y", default=1000)
|
||||
dpi: IntProperty(name="DPI", default=75, update=update_dpi)
|
||||
width: FloatProperty(name="Width", default=50, subtype="DISTANCE")
|
||||
height: FloatProperty(name="Height", default=50, subtype="DISTANCE")
|
||||
dpi: IntProperty(name="DPI", default=75, update=get_update_layer_callback("dpi", "DPI"))
|
||||
width: FloatProperty(name="Width", default=50, subtype="DISTANCE", update=update_width_height)
|
||||
height: FloatProperty(name="Height", default=50, subtype="DISTANCE", update=update_width_height)
|
||||
# Bonsai property is needed to prevent user from using unsupported panoramic camera.
|
||||
camera_type: EnumProperty(
|
||||
name="Camera Type",
|
||||
default="ORTHO",
|
||||
items=[(k, *v) for k, v in CAMERA_TYPE_ENUM_ITEMS.items()],
|
||||
update=update_camera_type,
|
||||
)
|
||||
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")
|
||||
update_props: BoolProperty(name="Enable Props Auto Update", default=True)
|
||||
update_props: BoolProperty(
|
||||
name="Enable Props Auto Update",
|
||||
description="Update related EPset_Drawing pset on any change in camera properties.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
linework_mode: Literal["OPENCASCADE", "FREESTYLE"]
|
||||
fill_mode: Literal["NONE", "SHAPELY", "SVGFILL"]
|
||||
cut_mode: Literal["BISECT", "OPENCASCADE"]
|
||||
|
||||
has_underlay: bool
|
||||
has_linework: bool
|
||||
has_annotation: bool
|
||||
target_view: TargetView
|
||||
|
||||
representation: str
|
||||
view_name: str
|
||||
diagram_scale: str
|
||||
custom_scale_numerator: str
|
||||
custom_scale_denominator: str
|
||||
raster_x: int
|
||||
raster_y: int
|
||||
dpi: int
|
||||
width: float
|
||||
height: float
|
||||
camera_type: CameraType
|
||||
is_nts: bool
|
||||
active_drawing_style_index: int
|
||||
filter_mode: str
|
||||
include_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
exclude_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
update_props: bool
|
||||
|
||||
def get_active_drawing_style(self) -> Union[DrawingStyle, None]:
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
return tool.Blender.get_active_uilist_element(dprops.drawing_styles, self.active_drawing_style_index)
|
||||
|
||||
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
|
||||
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
|
||||
def update_representation(self, obj):
|
||||
def update_representation(self, matrix_world: Matrix) -> bool:
|
||||
"""Update ``representation`` based on current camera properties and the provided world matrix.
|
||||
|
||||
:return: ``True`` if ``representation`` was updated and
|
||||
representation should also be updated in IFC.
|
||||
"""
|
||||
# Matrix is used instead of Object so this works before the Object exists,
|
||||
# allowing all camera initialization to stay encapsulated in `create_camera`.
|
||||
camera = self.id_data
|
||||
assert isinstance(camera, bpy.types.Camera)
|
||||
|
||||
# Rounding is necessary to avoid float garbage differences
|
||||
# forcing unnecessary representation update.
|
||||
def round_(f: float) -> float:
|
||||
return round(f, 6)
|
||||
|
||||
representation = json.dumps(
|
||||
{
|
||||
"matrix": [list(x) for x in obj.matrix_world],
|
||||
"type": self.camera_type,
|
||||
"matrix": [[round_(v) for v in row] for row in matrix_world],
|
||||
"raster_x": self.raster_x,
|
||||
"raster_y": self.raster_y,
|
||||
"ortho_scale": obj.data.ortho_scale,
|
||||
"clip_end": obj.data.clip_end,
|
||||
"ortho_scale": round_(camera.ortho_scale),
|
||||
"clip_end": round_(camera.clip_end),
|
||||
}
|
||||
)
|
||||
if self.representation != representation:
|
||||
@@ -539,6 +668,41 @@ class BIMCameraProperties(PropertyGroup):
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_camera_resolution(self) -> tuple[int, int]:
|
||||
"""Update ``camera.ortho_scale``, ``raster_x`` and ``raster_y``
|
||||
based on current ``width`` and ``height`` and diagram scale props.
|
||||
|
||||
:return: tuple[resolution_x, resolution_y]
|
||||
"""
|
||||
assert isinstance(camera := self.id_data, bpy.types.Camera)
|
||||
ortho_scale, aspect_ratio = self.get_scale_and_aspect_ratio()
|
||||
aspect_ratio = self.width / self.height
|
||||
|
||||
camera.ortho_scale = ortho_scale
|
||||
diagram_scale = tool.Drawing.get_diagram_scale(camera)
|
||||
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
|
||||
|
||||
if self.width > self.height:
|
||||
aspect_ratio = self.height / self.width
|
||||
raster_x = ortho_scale * scale_ratio * self.dpi / 0.0254
|
||||
raster_y = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254
|
||||
else:
|
||||
aspect_ratio = self.width / self.height
|
||||
raster_x = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254
|
||||
raster_y = ortho_scale * scale_ratio * self.dpi / 0.0254
|
||||
|
||||
raster_x, raster_y = int(raster_x), int(raster_y)
|
||||
self.raster_x, self.raster_y = raster_x, raster_y
|
||||
return raster_x, raster_y
|
||||
|
||||
def get_scale_and_aspect_ratio(self) -> tuple[float, float]:
|
||||
"""
|
||||
:return: A tuple of calculated ortho scale and aspect ratio values.
|
||||
"""
|
||||
ortho_scale = max(self.width, self.height)
|
||||
aspect_ratio = self.width / self.height
|
||||
return ortho_scale, aspect_ratio
|
||||
|
||||
|
||||
DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
|
||||
BOX_ALIGNMENT_POSITIONS = [
|
||||
@@ -596,6 +760,12 @@ class LiteralProps(PropertyGroup):
|
||||
}
|
||||
return text_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
value: str
|
||||
box_alignment: str
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
class BIMTextProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
@@ -621,7 +791,7 @@ class BIMTextProperties(PropertyGroup):
|
||||
|
||||
def get_text_edited_data(self) -> dict[str, Any]:
|
||||
"""should be called only if `is_editing`
|
||||
otherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
|
||||
otherwise should use `DecoratorData.get_text_data(obj)` instead
|
||||
because this data could be out of date
|
||||
"""
|
||||
literals_data = []
|
||||
@@ -646,6 +816,10 @@ class BIMAssignedProductProperties(PropertyGroup):
|
||||
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
|
||||
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object, poll=relating_product_poll)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing_product: bool
|
||||
relating_product: Union[bpy.types.Object, None]
|
||||
|
||||
|
||||
annotation_classes = [
|
||||
(x, *tool.Drawing.ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(tool.Drawing.ANNOTATION_TYPES_DATA)
|
||||
|
||||
@@ -90,7 +90,6 @@ class SheetBuilder:
|
||||
layout_dir = os.path.dirname(layout_path)
|
||||
|
||||
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
|
||||
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
|
||||
|
||||
if not os.path.exists(layout_path) or not os.path.exists(drawing_path):
|
||||
raise FileNotFoundError
|
||||
@@ -116,16 +115,6 @@ class SheetBuilder:
|
||||
|
||||
x, y = self.next_drawing_location(layout_root, view_width)
|
||||
|
||||
# add background
|
||||
if os.path.isfile(underlay_path):
|
||||
background = ET.SubElement(view, "image")
|
||||
background.attrib["data-type"] = "background"
|
||||
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
|
||||
background.attrib["x"] = str(x)
|
||||
background.attrib["y"] = str(y)
|
||||
background.attrib["width"] = str(view_width)
|
||||
background.attrib["height"] = str(view_height)
|
||||
|
||||
# add foreground
|
||||
if os.path.isfile(drawing_path):
|
||||
foreground = ET.SubElement(view, "image")
|
||||
@@ -406,14 +395,11 @@ class SheetBuilder:
|
||||
|
||||
images = view.findall("{http://www.w3.org/2000/svg}image")
|
||||
|
||||
background = None
|
||||
foreground = None
|
||||
view_title = None
|
||||
|
||||
for image in images:
|
||||
if image.attrib["data-type"] == "background":
|
||||
background = image
|
||||
elif image.attrib["data-type"] == "foreground":
|
||||
if image.attrib["data-type"] == "foreground":
|
||||
foreground = image
|
||||
elif image.attrib["data-type"] == "view-title":
|
||||
view_title = image
|
||||
@@ -423,12 +409,6 @@ class SheetBuilder:
|
||||
svg = self.ensure_drawing_unique_styles(svg, drawing_id)
|
||||
view.append(svg)
|
||||
|
||||
if background is not None:
|
||||
background_path = os.path.join(self.layout_dir, self.get_href(background))
|
||||
raster_path = os.path.join(self.sheets_dir, os.path.basename(background_path))
|
||||
shutil.copy(background_path, raster_path)
|
||||
self.references["RASTER"].append(raster_path)
|
||||
|
||||
if view_title is not None:
|
||||
foreground_path = self.get_href(foreground)
|
||||
data = reference.get_info()
|
||||
@@ -521,8 +501,14 @@ class SheetBuilder:
|
||||
self.scale = embedded.attrib.get("data-scale")
|
||||
images = embedded.findall("{http://www.w3.org/2000/svg}image")
|
||||
for image in images:
|
||||
new_href = ntpath.basename(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
|
||||
image.attrib["{http://www.w3.org/1999/xlink}href"] = new_href
|
||||
old_href = Path(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
|
||||
if not os.path.isabs(old_href):
|
||||
template_dir = Path(os.path.join(self.layout_dir, svg_path)).resolve().parent
|
||||
old_href = Path(os.path.join(template_dir, old_href))
|
||||
old_href = old_href.absolute().resolve().as_posix()
|
||||
new_href = Path(os.path.join(self.sheets_dir, Path(old_href).name)).absolute().resolve().as_posix()
|
||||
shutil.copy(old_href, new_href)
|
||||
image.attrib["{http://www.w3.org/1999/xlink}href"] = Path(old_href).name
|
||||
for child in embedded:
|
||||
if "namedview" in child.tag:
|
||||
continue
|
||||
|
||||
@@ -22,6 +22,7 @@ import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import shutil
|
||||
from bpy.types import SplineBezierPoints, SplinePoints
|
||||
import mathutils
|
||||
import xml.etree.ElementTree as ET
|
||||
import svgwrite
|
||||
@@ -37,7 +38,7 @@ from bonsai.bim.module.drawing.data import DrawingsData
|
||||
from bonsai.bim.module.drawing.data import DecoratorData
|
||||
from math import pi, ceil, atan, degrees, acos
|
||||
from mathutils import geometry, Vector
|
||||
from typing import Optional, Self
|
||||
from typing import Optional, Self, Union, Sequence, Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -59,14 +60,29 @@ class External(svgwrite.container.Group):
|
||||
|
||||
|
||||
class SvgWriter:
|
||||
def __init__(self):
|
||||
metadata: list[str]
|
||||
resource_paths: dict[tool.Drawing.ResourceType, Union[str, None]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
camera_width: float,
|
||||
camera_height: float,
|
||||
camera: bpy.types.Object,
|
||||
camera_projection: Vector,
|
||||
scale: float = 1 / 100, # 1:100
|
||||
human_scale: str = "NTS",
|
||||
):
|
||||
self.data_dir = None
|
||||
self.human_scale = "NTS"
|
||||
self.human_scale = human_scale
|
||||
self.metadata = []
|
||||
self.scale = 1 / 100 # 1:100
|
||||
self.camera_width = None
|
||||
self.camera_height = None
|
||||
self.scale = scale
|
||||
self.camera = camera
|
||||
self.camera_width = camera_width
|
||||
self.camera_height = camera_height
|
||||
self.camera_projection = camera_projection
|
||||
self.resource_paths = {}
|
||||
self.calculate_scale()
|
||||
|
||||
def create_blank_svg(self, output_path: str) -> Self:
|
||||
self.calculate_scale()
|
||||
@@ -90,7 +106,7 @@ class SvgWriter:
|
||||
|
||||
def setup_drawing_resource_paths(self, element: ifcopenshell.entity_instance) -> None:
|
||||
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
|
||||
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"):
|
||||
for resource in tool.Drawing.RESOURCE_TYPES:
|
||||
resource_path = pset.get(resource)
|
||||
if not resource_path:
|
||||
self.resource_paths[resource] = None
|
||||
@@ -176,12 +192,20 @@ class SvgWriter:
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def draw_annotations(self, annotations, precision, decimal_places):
|
||||
def draw_annotations(
|
||||
self,
|
||||
annotations: list[ifcopenshell.entity_instance],
|
||||
precision: Union[float, None],
|
||||
decimal_places: Union[int, None],
|
||||
) -> Self:
|
||||
self.precision = precision
|
||||
self.decimal_places = decimal_places
|
||||
for element in annotations:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING":
|
||||
if (
|
||||
not isinstance(obj, bpy.types.Object)
|
||||
or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING"
|
||||
):
|
||||
continue
|
||||
elif object_type == "GRID":
|
||||
self.draw_grid_annotation(obj)
|
||||
@@ -256,11 +280,12 @@ class SvgWriter:
|
||||
get_text, tag, dimension_data, text_position=text_position, angle=angle, class_str="SECTIONLEVEL"
|
||||
)
|
||||
|
||||
def draw_stair_annotation(self, obj):
|
||||
def draw_stair_annotation(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
matrix_world = obj.matrix_world
|
||||
classes = self.get_attribute_classes(obj)
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
@@ -284,7 +309,7 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
|
||||
def draw_grid_annotation(self, obj):
|
||||
def draw_grid_annotation(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
matrix_world = obj.matrix_world
|
||||
@@ -358,6 +383,7 @@ class SvgWriter:
|
||||
|
||||
def get_attribute_classes(self, obj: bpy.types.Object) -> list[str]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
global_id = "GlobalId-{}".format(element.GlobalId)
|
||||
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
|
||||
str(ifcopenshell.util.element.get_predefined_type(element))
|
||||
@@ -374,7 +400,7 @@ class SvgWriter:
|
||||
)
|
||||
return classes
|
||||
|
||||
def draw_line_annotation(self, obj):
|
||||
def draw_line_annotation(self, obj: bpy.types.Object) -> None:
|
||||
# TODO: properly scope these offsets
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
@@ -397,7 +423,7 @@ class SvgWriter:
|
||||
elif isinstance(obj.data, bpy.types.Mesh):
|
||||
self.draw_edge_annotation(obj, classes)
|
||||
|
||||
def draw_edge_annotation(self, obj, classes):
|
||||
def draw_edge_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
|
||||
predefined_type = classes[2].split("-", 1)[1]
|
||||
|
||||
x_offset = self.raw_width / 2
|
||||
@@ -415,7 +441,7 @@ class SvgWriter:
|
||||
self.svg.line(start=start * self.svg_scale, end=end * self.svg_scale, class_=" ".join(classes))
|
||||
)
|
||||
|
||||
def draw_batting_annotation():
|
||||
def draw_batting_annotation() -> None:
|
||||
v0_global = matrix_world @ obj.data.vertices[0].co.xyz
|
||||
v1_global = matrix_world @ obj.data.vertices[1].co.xyz
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
@@ -481,41 +507,42 @@ class SvgWriter:
|
||||
)
|
||||
self.svg.add(self.svg.polyline(points=points, class_=" ".join(classes), style=polyline_style))
|
||||
|
||||
def draw_revision_cloud_annotation():
|
||||
def draw_revision_cloud_annotation() -> None:
|
||||
segment_width = 15.0
|
||||
base_height = 1
|
||||
width = 5
|
||||
|
||||
def get_svg_half_circle(height, width):
|
||||
def get_svg_half_circle(height: float, width: float) -> str:
|
||||
cp0 = f"0,-{height}"
|
||||
cp1 = f"{width},-{height}"
|
||||
end_point = f"{width},0"
|
||||
circle = f"c{cp0} {cp1} {end_point}"
|
||||
return circle
|
||||
|
||||
def get_revision_pattern(base_offset):
|
||||
def get_revision_pattern(base_offset: Vector) -> str:
|
||||
pattern = f"m{base_offset.x},{base_offset.y}"
|
||||
pattern += " " + get_svg_half_circle(2 * base_height, width)
|
||||
pattern += " " + get_svg_half_circle(2.5 * base_height, width)
|
||||
pattern += " " + get_svg_half_circle(1.5 * base_height, width)
|
||||
return pattern
|
||||
|
||||
def get_scale(size, direction):
|
||||
def get_scale(size: float, direction: Vector) -> str:
|
||||
original_edge = direction * size
|
||||
current_svg_segments = ceil(size / segment_width) * segment_width * direction
|
||||
scale = [1 if original_edge[i] == 0 else original_edge[i] / current_svg_segments[i] for i in range(2)]
|
||||
return "scale(%f, %f)" % (scale[0], scale[1])
|
||||
|
||||
def poly_to_edges(poly):
|
||||
edges = []
|
||||
def poly_to_edges(poly: Sequence[int]) -> list[tuple[int, int]]:
|
||||
edges: list[tuple[int, int]] = []
|
||||
n_verts = len(poly)
|
||||
lats_index = n_verts - 1
|
||||
last_index = n_verts - 1
|
||||
for i in range(len(poly)):
|
||||
edge = [poly[i], (poly[i + 1]) if i != lats_index else poly[0]]
|
||||
edge = (poly[i], (poly[i + 1]) if i != last_index else poly[0])
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
safe_offset_x = 2.0
|
||||
marker_width = segment_width + safe_offset_x * 2
|
||||
market_height = 15.0
|
||||
@@ -573,7 +600,7 @@ class SvgWriter:
|
||||
)
|
||||
self.svg.add(polyline)
|
||||
|
||||
def draw_section_annotation():
|
||||
def draw_section_annotation() -> None:
|
||||
display_data = DecoratorData.get_section_markers_display_data(obj)
|
||||
connect_markers = display_data["connect_markers"]
|
||||
|
||||
@@ -624,7 +651,7 @@ class SvgWriter:
|
||||
for edge in obj.data.edges:
|
||||
draw_simple_edge_annotation(*edge.vertices[:])
|
||||
|
||||
def draw_leader_annotation(self, obj):
|
||||
def draw_leader_annotation(self, obj: bpy.types.Object) -> None:
|
||||
self.draw_line_annotation(obj)
|
||||
spline = obj.data.splines[0]
|
||||
spline_points = spline.bezier_points if spline.bezier_points else spline.points
|
||||
@@ -634,7 +661,7 @@ class SvgWriter:
|
||||
position = Vector((0, 0, 0))
|
||||
self.draw_text_annotation(obj, position)
|
||||
|
||||
def draw_section_annotation(self, obj):
|
||||
def draw_section_annotation(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
@@ -688,7 +715,7 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
|
||||
def draw_elevation_annotation(self, obj):
|
||||
def draw_elevation_annotation(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
symbol_position = self.project_point_onto_camera(obj.location)
|
||||
@@ -927,7 +954,7 @@ class SvgWriter:
|
||||
symbol_transform = self.get_symbol_transform(svg_position_str)
|
||||
self.draw_symbol(svg_id, symbol_transform)
|
||||
|
||||
def draw_break_annotations(self, obj):
|
||||
def draw_break_annotations(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
@@ -950,18 +977,20 @@ class SvgWriter:
|
||||
d = "M{}".format(d[1:])
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
|
||||
def draw_plan_level_annotation(self, obj):
|
||||
def draw_plan_level_annotation(self, obj: bpy.types.Object) -> None:
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
matrix_world = obj.matrix_world
|
||||
classes = self.get_attribute_classes(obj)
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
description = element.Description
|
||||
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
suppress_zero_inches = dimension_data["suppress_zero_inches"]
|
||||
base_offset_y = 1.0
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
@@ -1000,7 +1029,8 @@ class SvgWriter:
|
||||
box_alignment=box_alignment,
|
||||
)
|
||||
|
||||
def draw_angle_annotations(self, obj):
|
||||
def draw_angle_annotations(self, obj: bpy.types.Object) -> None:
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
points = obj.data.splines[0].points
|
||||
# [1, 2, 3, 4, 5] -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
|
||||
points_chunked = [points[i : i + 3] for i in range(len(points) - 2)]
|
||||
@@ -1009,18 +1039,18 @@ class SvgWriter:
|
||||
points_chunk = [obj.matrix_world @ p.co.xyz for p in points_chunk]
|
||||
self.draw_svg_3point_arc(obj, points_chunk)
|
||||
|
||||
def draw_svg_3point_arc(self, obj, angle_points):
|
||||
def draw_svg_3point_arc(self, obj: bpy.types.Object, angle_points: list[Vector]) -> None:
|
||||
"""`angle_points` are expected to be already in world space"""
|
||||
# This implementation uses an SVG arc, which means that it can only draw
|
||||
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
|
||||
# Gosh this is bad code :(
|
||||
|
||||
def position_on_svg(p):
|
||||
def position_on_svg(p: Vector) -> Vector:
|
||||
p = self.project_point_onto_camera(p)
|
||||
offset = Vector([self.raw_width / 2, self.raw_height / 2])
|
||||
return (offset + p.xy * Vector((1, -1))) * self.svg_scale
|
||||
|
||||
def get_angle_value():
|
||||
def get_angle_value() -> float:
|
||||
"""points should be in world space"""
|
||||
# calculate arc angle, need to make sure we do it in world space
|
||||
v0, v1, v2 = angle_points
|
||||
@@ -1068,7 +1098,7 @@ class SvgWriter:
|
||||
# When rewriting to use polylines, we should use this normal instead.
|
||||
# center = tool.Cad.get_center_of_arc([p.to_3d() for p in arc_points], None).to_2d()
|
||||
# normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center])
|
||||
# normal = Vector(self.camera_projection)
|
||||
# normal = self.camera_projection
|
||||
# dir1 = (arc_end_pts[0] - center).normalized()
|
||||
# arc_mid_dir = (arc_end_pts[1] - center).normalized()
|
||||
|
||||
@@ -1104,10 +1134,11 @@ class SvgWriter:
|
||||
classes = self.get_attribute_classes(obj)
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
|
||||
def draw_radius_annotations(self, obj):
|
||||
def draw_radius_annotations(self, obj: bpy.types.Object) -> None:
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
classes = self.get_attribute_classes(obj)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
tag = element.Description
|
||||
matrix_world = obj.matrix_world
|
||||
|
||||
@@ -1141,7 +1172,7 @@ class SvgWriter:
|
||||
get_text, tag, dimension_data, text_position=text_position, class_str="RADIUS", box_alignment="center"
|
||||
)
|
||||
|
||||
def draw_dimension_text(self, get_text, tag, dimension_data, **create_text_kwargs):
|
||||
def draw_dimension_text(self, get_text: Callable[[], str], tag, dimension_data, **create_text_kwargs) -> None:
|
||||
prefix = dimension_data["text_prefix"]
|
||||
suffix = dimension_data["text_suffix"]
|
||||
show_description_only = dimension_data["show_description_only"]
|
||||
@@ -1166,12 +1197,13 @@ class SvgWriter:
|
||||
for text in text_tags:
|
||||
self.svg.add(text)
|
||||
|
||||
def draw_fall_annotations(self, obj):
|
||||
def draw_fall_annotations(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
classes = self.get_attribute_classes(obj)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
@@ -1236,13 +1268,14 @@ class SvgWriter:
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
|
||||
|
||||
def draw_diameter_annotations(self, obj):
|
||||
def draw_diameter_annotations(self, obj: bpy.types.Object) -> None:
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
dimension_text = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
for i, p in enumerate(points):
|
||||
@@ -1264,13 +1297,14 @@ class SvgWriter:
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
|
||||
def draw_dimension_annotations(self, obj):
|
||||
def draw_dimension_annotations(self, obj: bpy.types.Object) -> None:
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
dimension_text = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
for i in range(len(points) - 1):
|
||||
@@ -1289,7 +1323,7 @@ class SvgWriter:
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
|
||||
def draw_measureit_arch_dimension_annotations(self):
|
||||
def draw_measureit_arch_dimension_annotations(self) -> None:
|
||||
try:
|
||||
import MeasureIt_ARCH.measureit_arch_external_utils
|
||||
|
||||
@@ -1303,9 +1337,9 @@ class SvgWriter:
|
||||
|
||||
def draw_dimension_annotation(
|
||||
self,
|
||||
v0_global,
|
||||
v1_global,
|
||||
classes,
|
||||
v0_global: Vector,
|
||||
v1_global: Vector,
|
||||
classes: list[str],
|
||||
dimension_text=None,
|
||||
text_format=lambda x: x,
|
||||
show_description_only=False,
|
||||
@@ -1314,7 +1348,7 @@ class SvgWriter:
|
||||
text_suffix="",
|
||||
fill_bg=False,
|
||||
custom_unit=None,
|
||||
):
|
||||
) -> None:
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
v1 = self.project_point_onto_camera(v1_global)
|
||||
@@ -1441,12 +1475,12 @@ class SvgWriter:
|
||||
# TODO is this needlessly complex?
|
||||
return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
|
||||
point.xyz,
|
||||
point.xyz - Vector(self.camera_projection),
|
||||
point.xyz - self.camera_projection,
|
||||
self.camera.location,
|
||||
Vector(self.camera_projection),
|
||||
self.camera_projection,
|
||||
)
|
||||
|
||||
def get_spline_points(self, spline):
|
||||
def get_spline_points(self, spline: bpy.types.Spline) -> Union[SplineBezierPoints, SplinePoints]:
|
||||
return spline.bezier_points if spline.bezier_points else spline.points
|
||||
|
||||
def draw_polygon(self, polygon, position):
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
@@ -28,6 +29,10 @@ from bonsai.bim.module.drawing.data import (
|
||||
ElementFiltersData,
|
||||
DecoratorData,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.drawing.prop import DocProperties, Drawing
|
||||
|
||||
|
||||
class BIM_PT_camera(Panel):
|
||||
@@ -39,18 +44,21 @@ class BIM_PT_camera(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_drawings"
|
||||
|
||||
def draw(self, context):
|
||||
if not (context.scene.camera and hasattr(context.scene.camera.data, "BIMCameraProperties")):
|
||||
assert context.scene and self.layout
|
||||
camera = context.scene.camera
|
||||
if not camera:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active Drawing", icon="ERROR")
|
||||
return
|
||||
|
||||
if "/" not in context.scene.camera.name:
|
||||
if not tool.Ifc.get_entity(camera):
|
||||
self.layout.label(text="This is not a BIM camera.")
|
||||
return
|
||||
|
||||
assert isinstance(camera_data := camera.data, bpy.types.Camera)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
self.layout.use_property_split = True
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
props = context.scene.camera.data.BIMCameraProperties
|
||||
|
||||
col = self.layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
@@ -63,6 +71,13 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
|
||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "target_view")
|
||||
|
||||
if props.target_view == "MODEL_VIEW":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "camera_type")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "linework_mode")
|
||||
if props.linework_mode == "OPENCASCADE":
|
||||
@@ -77,7 +92,7 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(props, "height")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.camera.data, "clip_end", text="Depth")
|
||||
row.prop(camera_data, "clip_end", text="Depth")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "diagram_scale", text="Scale")
|
||||
@@ -110,7 +125,8 @@ class BIM_PT_element_filters(Panel):
|
||||
if not ElementFiltersData.is_loaded:
|
||||
ElementFiltersData.load()
|
||||
|
||||
props = context.scene.camera.data.BIMCameraProperties
|
||||
assert context.scene and (camera := context.scene.camera)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
|
||||
if props.filter_mode == "INCLUDE":
|
||||
bonsai.bim.helper.draw_filter(
|
||||
@@ -157,13 +173,13 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
camera = context.scene.camera
|
||||
assert camera
|
||||
assert context.scene and (camera := context.scene.camera)
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
props = camera.data.BIMCameraProperties
|
||||
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
drawing_style = props.get_active_drawing_style()
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
@@ -177,7 +193,7 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
row.label(text="Current Shading Style:")
|
||||
row.label(text=current_shading_style)
|
||||
row.operator("bim.add_drawing_style", icon="ADD", text="")
|
||||
if drawing_index_is_valid:
|
||||
if drawing_style:
|
||||
row.operator("bim.remove_drawing_style", icon="X", text="").index = props.active_drawing_style_index
|
||||
row.operator("bim.reload_drawing_styles", icon="FILE_REFRESH", text="")
|
||||
|
||||
@@ -192,9 +208,8 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
"active_drawing_style_index",
|
||||
)
|
||||
|
||||
if not drawing_index_is_valid:
|
||||
if drawing_style is None:
|
||||
return
|
||||
drawing_style = dprops.drawing_styles[props.active_drawing_style_index]
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "name")
|
||||
@@ -206,14 +221,6 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "exclude_query")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.add_drawing_style_attribute")
|
||||
|
||||
for index, attribute in enumerate(drawing_style.attributes):
|
||||
row = layout.row(align=True)
|
||||
row.prop(attribute, "name", text="")
|
||||
row.operator("bim.remove_drawing_style_attribute", icon="X", text="").index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.save_drawing_style")
|
||||
row.operator("bim.activate_drawing_style")
|
||||
@@ -266,9 +273,6 @@ class BIM_PT_drawings(Panel):
|
||||
row3 = row.row(align=True)
|
||||
row3.alignment = "RIGHT"
|
||||
|
||||
row3.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="").drawing = (
|
||||
active_drawing.ifc_definition_id
|
||||
)
|
||||
row3.operator("bim.activate_model", icon="VIEW3D", text="")
|
||||
|
||||
row3.separator(factor=0.5, type="SPACE")
|
||||
@@ -490,17 +494,19 @@ class BIM_PT_product_assignments(Panel):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get() or not context.active_object:
|
||||
return
|
||||
return False
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
return False
|
||||
return element.is_a("IfcAnnotation")
|
||||
|
||||
def draw(self, context):
|
||||
if not ProductAssignmentsData.is_loaded:
|
||||
ProductAssignmentsData.load()
|
||||
|
||||
props = context.active_object.BIMAssignedProductProperties
|
||||
assert self.layout
|
||||
assert (obj := context.active_object)
|
||||
props = tool.Drawing.get_object_assigned_product_props(obj)
|
||||
|
||||
if props.is_editing_product:
|
||||
row = self.layout.row(align=True)
|
||||
@@ -586,7 +592,7 @@ class BIM_PT_text(Panel):
|
||||
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
|
||||
|
||||
else:
|
||||
text_data = DecoratorData.get_ifc_text_data(obj)
|
||||
text_data = DecoratorData.get_text_data(obj)
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
|
||||
@@ -607,7 +613,16 @@ class BIM_PT_text(Panel):
|
||||
|
||||
|
||||
class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
def draw_item(
|
||||
self,
|
||||
context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: DocProperties,
|
||||
item: Drawing,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
):
|
||||
if not item:
|
||||
layout.label(text="", translate=False)
|
||||
return
|
||||
@@ -617,14 +632,22 @@ class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
row.label(text="", icon="BLANK1")
|
||||
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
|
||||
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
|
||||
row.separator(factor=0.5, type="SPACE")
|
||||
row.prop(item, "name", text="", emboss=False)
|
||||
row.separator(factor=0.25, type="SPACE")
|
||||
self.props = tool.Drawing.get_document_props()
|
||||
if (
|
||||
self.props.drawings
|
||||
and self.props.active_drawing_id
|
||||
and item.ifc_definition_id == self.props.active_drawing_id
|
||||
):
|
||||
row.label(text="", icon="OUTLINER_OB_CAMERA")
|
||||
row.operator("bim.activate_drawing", text="", icon="VIEW_CAMERA", emboss=True, depress=True).drawing = (
|
||||
item.ifc_definition_id
|
||||
)
|
||||
else:
|
||||
row.operator("bim.activate_drawing", text="", icon="VIEW_CAMERA_UNSELECTED", emboss=False).drawing = (
|
||||
item.ifc_definition_id
|
||||
)
|
||||
else:
|
||||
if item.target_view == "PLAN_VIEW":
|
||||
icon = "UV_FACESEL"
|
||||
|
||||
@@ -218,7 +218,7 @@ class AnnotationToolUI:
|
||||
|
||||
@classmethod
|
||||
def draw_edit_object_interface(cls, context):
|
||||
if DecoratorData.get_ifc_text_data(bpy.context.active_object):
|
||||
if DecoratorData.get_text_data(bpy.context.active_object):
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
||||
|
||||
@classmethod
|
||||
@@ -311,7 +311,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not bpy.context.active_object:
|
||||
return
|
||||
|
||||
if DecoratorData.get_ifc_text_data(bpy.context.active_object):
|
||||
if DecoratorData.get_text_data(bpy.context.active_object):
|
||||
bpy.ops.bim.edit_text_popup()
|
||||
|
||||
def hotkey_S_G(self):
|
||||
|
||||
@@ -56,7 +56,7 @@ classes = (
|
||||
operator.OverrideMeshSeparate,
|
||||
operator.OverrideModeSetEdit,
|
||||
operator.OverrideModeSetObject,
|
||||
operator.OverrideMove,
|
||||
operator.OverrideMoveSelect,
|
||||
operator.OverrideMoveMacro,
|
||||
operator.OverrideOriginSet,
|
||||
operator.OverrideOutlinerDelete,
|
||||
@@ -95,7 +95,7 @@ classes = (
|
||||
)
|
||||
|
||||
|
||||
addon_keymaps = []
|
||||
addon_keymaps: list[tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
||||
|
||||
|
||||
@persistent
|
||||
@@ -124,9 +124,9 @@ def register():
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move_select")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
|
||||
operator.OverrideMoveMacro.define("BIM_OT_override_move")
|
||||
operator.OverrideMoveMacro.define("BIM_OT_override_move_select")
|
||||
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
|
||||
|
||||
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
|
||||
@@ -156,11 +156,19 @@ def register():
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
# Deletion.
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS", shift=True)
|
||||
kmi.properties.use_global = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
|
||||
kmi.properties.confirm = False
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS", shift=True)
|
||||
kmi.properties.confirm = False
|
||||
kmi.properties.use_global = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
|
||||
@@ -23,6 +23,7 @@ import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
from typing import Any, Union
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
@@ -45,13 +46,12 @@ class ViewportData:
|
||||
cls.data = {"mode": cls.mode()}
|
||||
|
||||
@classmethod
|
||||
def mode(cls):
|
||||
def mode(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
obj_mode = ("OBJECT", "IFC Object Mode", "View and move the placements of objects", "OBJECT_DATAMODE", 0)
|
||||
item_mode = ("ITEM", "IFC Item Mode", "View individual representation items", "MESH_DATA", 1)
|
||||
edit_mode = ("EDIT", "IFC Edit Mode", "Edit representation items", "EDITMODE_HLT", 2)
|
||||
|
||||
obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
modes: list[tuple[str, str, str, str, int]] = [obj_mode]
|
||||
gprops = tool.Geometry.get_geometry_props()
|
||||
@@ -61,13 +61,14 @@ class ViewportData:
|
||||
if not obj:
|
||||
return modes
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
pprops = tool.Project.get_project_props()
|
||||
if obj in pprops.clipping_planes_objs:
|
||||
pass
|
||||
elif element:
|
||||
if tool.Geometry.is_locked(element):
|
||||
pass
|
||||
elif obj.data and tool.Geometry.is_profile_based(obj.data):
|
||||
elif obj.data and tool.Geometry.has_mesh_properties(obj.data) and tool.Geometry.is_profile_based(obj.data):
|
||||
modes.append(edit_mode)
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
modes.append(edit_mode)
|
||||
@@ -88,7 +89,7 @@ class ViewportData:
|
||||
|
||||
|
||||
class RepresentationsData:
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
@@ -102,10 +103,12 @@ class RepresentationsData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def representations(cls):
|
||||
results = []
|
||||
def representations(cls) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
|
||||
active_representation_id = None
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
@@ -132,8 +135,8 @@ class RepresentationsData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def contexts(cls):
|
||||
results = []
|
||||
def contexts(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
results: list[tuple[str, str, str]] = []
|
||||
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
results.append((str(element.id()), element.ContextType or "Unnamed", ""))
|
||||
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationSubContext", include_subtypes=False):
|
||||
@@ -219,9 +222,12 @@ class ConnectionsData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def connections(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
def connections(cls) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
obj = bpy.context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
|
||||
connected_to = getattr(element, "ConnectedTo", [])
|
||||
connected_from = getattr(element, "ConnectedFrom", [])
|
||||
@@ -285,13 +291,16 @@ class ConnectionsData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def is_connection_realization(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
def is_connection_realization(cls) -> Union[list[dict[str, Any]], None]:
|
||||
obj = bpy.context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
connections = getattr(element, "IsConnectionRealization", None)
|
||||
if not connections:
|
||||
return
|
||||
|
||||
results = []
|
||||
results: list[dict[str, Any]] = []
|
||||
for rel in connections:
|
||||
data = {
|
||||
"realizing_elements_connection_type": rel.ConnectionType,
|
||||
@@ -322,16 +331,19 @@ class DerivedCoordinatesData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def load_z_values(cls):
|
||||
def load_z_values(cls) -> None:
|
||||
cls.z_values = [
|
||||
(bpy.context.active_object.matrix_world @ Vector(co))[2] for co in bpy.context.active_object.bound_box
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def load_collection(cls):
|
||||
def load_collection(cls) -> None:
|
||||
cls.collection = None
|
||||
cls.collection_z = 0
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
parent = ifcopenshell.util.element.get_aggregate(element)
|
||||
@@ -392,7 +404,7 @@ class PlacementData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"has_placement": cls.has_placement()}
|
||||
cls.data: dict[str, Any] = {"has_placement": cls.has_placement()}
|
||||
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
obj = bpy.context.active_object
|
||||
@@ -415,13 +427,14 @@ class PlacementData:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def original_xyz(cls, obj):
|
||||
def original_xyz(cls, obj: bpy.types.Object) -> list[float]:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
translation = obj.matrix_world.translation
|
||||
xyz = ifcopenshell.util.geolocation.xyz2enh(
|
||||
obj.matrix_world[0][3],
|
||||
obj.matrix_world[1][3],
|
||||
obj.matrix_world[2][3],
|
||||
translation[0],
|
||||
translation[1],
|
||||
translation[2],
|
||||
float(props.blender_offset_x) * unit_scale,
|
||||
float(props.blender_offset_y) * unit_scale,
|
||||
float(props.blender_offset_z) * unit_scale,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,13 +35,13 @@ from bpy.props import (
|
||||
from typing import Optional, TYPE_CHECKING, Union, Literal
|
||||
|
||||
|
||||
def get_contexts(self, context):
|
||||
def get_contexts(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
if not RepresentationsData.is_loaded:
|
||||
RepresentationsData.load()
|
||||
return RepresentationsData.data["contexts"]
|
||||
|
||||
|
||||
def update_mode(self, context):
|
||||
def update_mode(self: "BIMGeometryProperties", context: bpy.types.Context) -> None:
|
||||
if self.is_changing_mode:
|
||||
return
|
||||
if context.mode.startswith("EDIT"):
|
||||
@@ -62,7 +62,7 @@ def update_mode(self, context):
|
||||
bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT")
|
||||
|
||||
|
||||
def update_representation_obj(self, context):
|
||||
def update_representation_obj(self: "BIMGeometryProperties", context: bpy.types.Context) -> None:
|
||||
for item_obj in self.item_objs:
|
||||
if item_obj.obj:
|
||||
data = item_obj.obj.data
|
||||
@@ -76,13 +76,13 @@ def update_representation_obj(self, context):
|
||||
self.is_changing_mode = False
|
||||
|
||||
|
||||
def get_mode(self, context):
|
||||
def get_mode(self: "BIMGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str, str, int]]:
|
||||
if not ViewportData.is_loaded:
|
||||
ViewportData.load()
|
||||
return ViewportData.data["mode"]
|
||||
|
||||
|
||||
def get_styles(self, context):
|
||||
def get_styles(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
# postponed import to avoid circular import
|
||||
from bonsai.bim.module.material.data import MaterialsData
|
||||
|
||||
@@ -91,13 +91,13 @@ def get_styles(self, context):
|
||||
return MaterialsData.data["styles"]
|
||||
|
||||
|
||||
def get_shape_aspects(self, context):
|
||||
def get_shape_aspects(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
if not RepresentationsData.is_loaded:
|
||||
RepresentationsData.load()
|
||||
return RepresentationsData.data["shape_aspects"]
|
||||
|
||||
|
||||
def get_material_constituents(self, context, edit_text):
|
||||
def get_material_constituents(self: "ShapeAspect", context: bpy.types.Context, edit_text: str) -> list[str]:
|
||||
from bonsai.bim.module.material.data import ObjectMaterialData
|
||||
|
||||
if not ObjectMaterialData.is_loaded:
|
||||
@@ -105,7 +105,7 @@ def get_material_constituents(self, context, edit_text):
|
||||
return ObjectMaterialData.data["active_material_constituents"]
|
||||
|
||||
|
||||
def get_layers(self, context):
|
||||
def get_layers(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
from bonsai.bim.module.layer.data import LayersData
|
||||
|
||||
if not LayersData.is_loaded:
|
||||
@@ -113,7 +113,7 @@ def get_layers(self, context):
|
||||
return LayersData.data["layers_enum"]
|
||||
|
||||
|
||||
def get_layers_no_active(self, context):
|
||||
def get_layers_no_active(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
from bonsai.bim.module.layer.data import LayersData
|
||||
|
||||
if not LayersData.is_loaded:
|
||||
@@ -162,6 +162,11 @@ class RepresentationItemObject(PropertyGroup):
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
ifc_definition_id: IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
obj: Union[bpy.types.Object, None]
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
class ShapeAspect(PropertyGroup):
|
||||
name: StringProperty(
|
||||
@@ -304,6 +309,14 @@ class BIMGeometryProperties(PropertyGroup):
|
||||
blender_item.name = name
|
||||
return blender_item
|
||||
|
||||
def remove_item_object_by_entity(self, item: ifcopenshell.entity_instance) -> None:
|
||||
ifc_id = item.id()
|
||||
for i, item_obj in enumerate(self.item_objs):
|
||||
if item_obj.ifc_definition_id == ifc_id:
|
||||
self.item_objs.remove(i)
|
||||
return
|
||||
assert False
|
||||
|
||||
def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool:
|
||||
return bool(obj != bpy.context.active_object and obj.data)
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ def object_menu(self, context):
|
||||
self.layout.operator("bim.override_object_delete", icon="PLUGIN")
|
||||
self.layout.operator("bim.override_paste_buffer", icon="PLUGIN")
|
||||
self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN")
|
||||
self.layout.menu("BIM_MT_separate", icon="PLUGIN")
|
||||
|
||||
|
||||
def edit_mesh_menu(self, context):
|
||||
@@ -76,25 +77,20 @@ class BIM_MT_separate(Menu):
|
||||
bl_label = "IFC Separate"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC Selection").type = "SELECTED"
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Material").type = "MATERIAL"
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE"
|
||||
assert self.layout
|
||||
self.layout.label(text="IFC Separate", icon_value=bonsai.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator_enum("bim.override_mesh_separate", "type")
|
||||
|
||||
|
||||
# TODO: remove as it's the same as BIM_MT_separate?
|
||||
class BIM_MT_hotkey_separate(Menu):
|
||||
bl_idname = "BIM_MT_hotkey_separate"
|
||||
bl_label = "Separate"
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
self.layout.label(text="IFC Separate", icon_value=bonsai.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"
|
||||
self.layout.operator_enum("bim.override_mesh_separate", "type")
|
||||
|
||||
|
||||
class BIM_MT_object_set_origin(Menu):
|
||||
@@ -102,21 +98,9 @@ class BIM_MT_object_set_origin(Menu):
|
||||
bl_label = "IFC Set Origin"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Geometry to Origin").origin_type = (
|
||||
"GEOMETRY_ORIGIN"
|
||||
)
|
||||
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Geometry").origin_type = (
|
||||
"ORIGIN_GEOMETRY"
|
||||
)
|
||||
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to 3D Cursor").origin_type = (
|
||||
"ORIGIN_CURSOR"
|
||||
)
|
||||
self.layout.operator(
|
||||
"bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Surface)"
|
||||
).origin_type = "ORIGIN_CENTER_OF_MASS"
|
||||
self.layout.operator(
|
||||
"bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Volume)"
|
||||
).origin_type = "ORIGIN_CENTER_OF_VOLUME"
|
||||
assert self.layout
|
||||
self.layout.label(text="IFC Set Origin", icon_value=bonsai.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator_enum("bim.override_origin_set", property="origin_type")
|
||||
|
||||
|
||||
def outliner_menu(self, context):
|
||||
|
||||
@@ -112,7 +112,9 @@ def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.t
|
||||
props.is_updating_coordinates = True
|
||||
blender_coordinates = tool.Georeference.get_coordinates("blender")
|
||||
local_coordinates = ifcopenshell.util.geolocation.xyz2enh(
|
||||
*blender_coordinates,
|
||||
blender_coordinates[0],
|
||||
blender_coordinates[1],
|
||||
blender_coordinates[2],
|
||||
float(props.blender_offset_x),
|
||||
float(props.blender_offset_y),
|
||||
float(props.blender_offset_z),
|
||||
|
||||
@@ -38,6 +38,7 @@ classes = (
|
||||
operator.RefreshGit,
|
||||
operator.SwitchRevision,
|
||||
operator.InstallGit,
|
||||
operator.RunGitDiff,
|
||||
prop.IfcGitTag,
|
||||
prop.IfcGitListItem,
|
||||
prop.IfcGitProperties,
|
||||
|
||||
@@ -409,3 +409,25 @@ class InstallGit(bpy.types.Operator):
|
||||
core.install_git(tool.IfcGit, self)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RunGitDiff(bpy.types.Operator):
|
||||
"""Run `git diff` for the current version of IFC file and the last saved one."""
|
||||
|
||||
bl_label = "Git Diff"
|
||||
bl_idname = "ifcgit.git_diff"
|
||||
bl_options = set()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC file loaded.")
|
||||
return False
|
||||
if not tool.Ifc.get_path():
|
||||
cls.poll_message_set("Current IFC file was never saved.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
core.run_git_diff(tool.IfcGit, self)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -283,3 +283,5 @@ class IFCGIT_PT_revision_inspector(bpy.types.Panel):
|
||||
"ifcgit.object_log",
|
||||
icon="TEXT",
|
||||
)
|
||||
row = layout.row()
|
||||
row.operator("ifcgit.git_diff")
|
||||
|
||||
@@ -392,6 +392,7 @@ ground_glow source ground
|
||||
scene.add_surface(scene_path)
|
||||
scene.add_source(sky_file_path)
|
||||
print("Setting up view...")
|
||||
assert isinstance(camera.data, bpy.types.Camera)
|
||||
if camera.data.type == "PERSP":
|
||||
# Perspective camera
|
||||
camera_fov = camera.data.angle
|
||||
@@ -547,9 +548,11 @@ class ViewFromSun(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
if not (camera := bpy.data.objects.get("SunPathCamera")):
|
||||
camera = bpy.data.objects.new("SunPathCamera", bpy.data.cameras.new("SunPathCamera"))
|
||||
assert isinstance(camera.data, bpy.types.Camera)
|
||||
assert context.scene
|
||||
camera.data.type = "ORTHO"
|
||||
camera.data.ortho_scale = 100 # The default of 6m is too small
|
||||
bpy.context.scene.collection.objects.link(camera)
|
||||
context.scene.collection.objects.link(camera)
|
||||
tool.Blender.activate_camera(camera)
|
||||
props = context.scene.BIMSolarProperties
|
||||
props.hour = props.hour # Just to refresh camera position
|
||||
|
||||
@@ -394,7 +394,7 @@ class ObjectMaterialData:
|
||||
return [(m, m, ifcopenshell.util.doc.get_entity_doc(version, m).get("description", "")) for m in material_types]
|
||||
|
||||
@classmethod
|
||||
def active_material_constituents(cls):
|
||||
def active_material_constituents(cls) -> list[str]:
|
||||
material = cls.material
|
||||
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
|
||||
return []
|
||||
|
||||
@@ -625,6 +625,15 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
|
||||
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
|
||||
|
||||
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
|
||||
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
|
||||
ifcopenshell.api.material.edit_profile_usage(
|
||||
self.file,
|
||||
usage=material_set_usage,
|
||||
attributes=attributes,
|
||||
)
|
||||
model_profile.DumbProfileRecalculator().recalculate(objects)
|
||||
|
||||
bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name)
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import bonsai.core.misc as core
|
||||
import bonsai.core.geometry as core_geometry
|
||||
import bonsai.core.root
|
||||
from mathutils import Vector, Matrix, Euler
|
||||
from typing import TYPE_CHECKING, Literal, get_args
|
||||
from typing_extensions import assert_never
|
||||
|
||||
|
||||
class SetOverrideColour(bpy.types.Operator):
|
||||
@@ -117,23 +119,45 @@ class ResizeToStorey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.resize_to_storey(tool.Misc, tool.Ifc, obj=obj, total_storeys=self.total_storeys)
|
||||
|
||||
|
||||
SplitAlongEdgeMode = Literal["BOOLEAN", "BISECT"]
|
||||
|
||||
|
||||
class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.split_along_edge"
|
||||
bl_label = "Split Along Edge"
|
||||
bl_description = (
|
||||
"Active object is considered to be a cutting object."
|
||||
"Split selected objects by the face of the cutter object.\n"
|
||||
"Active object is considered to be a cutting object. "
|
||||
"It can be a simple Blender object (e.g. a plane), not connected to IFC. "
|
||||
"Will unassign element from a type if type has a representation."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
mode: bpy.props.StringProperty(default="BOOLEAN")
|
||||
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
default="BOOLEAN",
|
||||
items=tuple((i, i, "") for i in get_args(SplitAlongEdgeMode)),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
mode: SplitAlongEdgeMode
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not (obj := context.active_object) or obj.type != "MESH":
|
||||
cls.poll_message_set("No active mesh object is selected.")
|
||||
return False
|
||||
if not context.selected_objects:
|
||||
cls.poll_message_set("No objects selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
cutter = context.active_object
|
||||
objs = [o for o in context.selected_objects if o != cutter]
|
||||
assert cutter
|
||||
|
||||
objs = [o for o in context.selected_objects if o != cutter and o.type == "MESH"]
|
||||
if not objs:
|
||||
self.report({"ERROR"}, "No other mesh objects selected besides the cutter object.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not tool.Ifc.get():
|
||||
if self.mode == "BOOLEAN":
|
||||
@@ -142,9 +166,11 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Misc.bisect_objects_with_cutter(objs, cutter)
|
||||
for obj in objs:
|
||||
bpy.data.objects.remove(obj)
|
||||
else:
|
||||
assert_never(self.mode)
|
||||
return
|
||||
|
||||
objs_to_cut = []
|
||||
objs_to_cut: list[bpy.types.Object] = []
|
||||
# Splitting only works on meshes
|
||||
for obj in objs:
|
||||
# You cannot split meshes if the representation is mapped.
|
||||
@@ -183,6 +209,8 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
|
||||
new_objs = tool.Misc.boolean_objects_with_cutter(objs_to_cut, cutter)
|
||||
elif self.mode == "BISECT":
|
||||
new_objs = tool.Misc.bisect_objects_with_cutter(objs_to_cut, cutter)
|
||||
else:
|
||||
assert_never(self.mode)
|
||||
|
||||
for obj in new_objs:
|
||||
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
|
||||
@@ -191,6 +219,7 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
assert representation
|
||||
core_geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
|
||||
@@ -77,6 +77,7 @@ classes = (
|
||||
wall.ExtendWallsToWall,
|
||||
wall.FlipWall,
|
||||
wall.MergeWall,
|
||||
wall.OffsetWalls,
|
||||
wall.RecalculateWall,
|
||||
wall.SplitWall,
|
||||
wall.UnjoinWalls,
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
@@ -33,8 +34,23 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert (obj := context.active_object)
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
allowed_types = (
|
||||
"IfcElement",
|
||||
"IfcAnnotation",
|
||||
"IfcOpeningElement",
|
||||
"IfcSpatialElement",
|
||||
)
|
||||
|
||||
if not any(element.is_a(c) for c in allowed_types):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Adding array to element of type '{element.is_a()}' is not supported. Supported types: {','.join(allowed_types)}.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
array = {
|
||||
"children": [],
|
||||
@@ -54,14 +70,13 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
data.append(array)
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Array")
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Array")
|
||||
data = [array]
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties={"Parent": element.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
properties={"Parent": element.GlobalId, "Data": ifc_file.create_entity("IfcText", json.dumps(data))},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -616,6 +616,9 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_editing = False
|
||||
|
||||
update_door_modifier_representation(obj)
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type:
|
||||
tool.Model.mark_thumbnail_for_update(element_type)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
|
||||
|
||||
@@ -18,14 +18,18 @@
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.grid
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.root
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty
|
||||
from mathutils import Vector
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
def add_object(self, context):
|
||||
def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
obj = bpy.data.objects.new("Grid", None)
|
||||
obj.name = "Grid"
|
||||
|
||||
@@ -33,6 +37,11 @@ def add_object(self, context):
|
||||
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcGrid", should_add_representation=False
|
||||
)
|
||||
grid = tool.Ifc.get_entity(obj)
|
||||
assert grid
|
||||
|
||||
# Requirement in IFC4+.
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj)
|
||||
|
||||
for i in range(0, self.total_u):
|
||||
verts = [
|
||||
@@ -46,9 +55,7 @@ def add_object(self, context):
|
||||
tag = chr(ord("A") + i)
|
||||
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
|
||||
|
||||
result = ifcopenshell.api.run(
|
||||
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="UAxes", grid=grid
|
||||
)
|
||||
result = ifcopenshell.api.grid.create_grid_axis(ifc_file, axis_tag=tag, uvw_axes="UAxes", grid=grid)
|
||||
tool.Ifc.link(result, obj)
|
||||
tool.Model.create_axis_curve(obj, result)
|
||||
tool.Collector.assign(obj)
|
||||
@@ -65,9 +72,7 @@ def add_object(self, context):
|
||||
tag = str(i + 1).zfill(2)
|
||||
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
|
||||
|
||||
result = ifcopenshell.api.run(
|
||||
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="VAxes", grid=grid
|
||||
)
|
||||
result = ifcopenshell.api.grid.create_grid_axis(ifc_file, axis_tag=tag, uvw_axes="VAxes", grid=grid)
|
||||
tool.Ifc.link(result, obj)
|
||||
tool.Model.create_axis_curve(obj, result)
|
||||
tool.Collector.assign(obj)
|
||||
@@ -78,13 +83,20 @@ def add_object(self, context):
|
||||
class BIM_OT_add_object(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_grid"
|
||||
bl_label = "Grid"
|
||||
bl_description = "Add IfcGrid."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
u_spacing: FloatProperty(name="U Spacing", default=10)
|
||||
u_spacing: FloatProperty(name="U Spacing", default=10, subtype="DISTANCE")
|
||||
total_u: IntProperty(name="Number of U Grids", default=3)
|
||||
v_spacing: FloatProperty(name="V Spacing", default=10)
|
||||
v_spacing: FloatProperty(name="V Spacing", default=10, subtype="DISTANCE")
|
||||
total_v: IntProperty(name="Number of V Grids", default=3)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
u_spacing: float
|
||||
total_u: int
|
||||
v_spacing: float
|
||||
total_v: int
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get() and context.mode == "OBJECT"
|
||||
|
||||
@@ -36,12 +36,6 @@ def load_post(*args):
|
||||
"sequence.edit_task_time", "Bonsai.Task.CalculateQuantities", task.calculate_quantities
|
||||
)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"material.edit_profile_usage",
|
||||
"Bonsai.Product.RegenerateProfileUsage",
|
||||
product.regenerate_profile_usage,
|
||||
)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"type.assign_type", "Bonsai.DumbWall.RegenerateFromType", wall.DumbWallPlaner().regenerate_from_type
|
||||
)
|
||||
|
||||
@@ -484,32 +484,21 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
||||
|
||||
class ShowOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.show_openings"
|
||||
bl_label = "Show"
|
||||
bl_description = "Show Openings"
|
||||
bl_label = "Show Openings"
|
||||
bl_description = "Show openings for selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
objs = list(context.selected_objects)
|
||||
# If several parts of an aggregation are selected, the aggregate openings may be queried more than once
|
||||
objects_element_map = set()
|
||||
while objs:
|
||||
obj = objs.pop(0)
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
if getattr(element, "Decomposes", None):
|
||||
# Select aggregate recursively
|
||||
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
objs.append(aggregate_obj)
|
||||
objects_element_map.add((obj, element))
|
||||
|
||||
for obj, element in objects_element_map:
|
||||
if element is None:
|
||||
continue
|
||||
self.show_object_openings(obj, element)
|
||||
DecorationsHandler.install(bpy.context)
|
||||
bpy.ops.bim.update_openings_focus()
|
||||
return {"FINISHED"}
|
||||
|
||||
def show_object_openings(self, obj, element):
|
||||
def show_object_openings(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
openings_elements = [rel.RelatedOpeningElement for rel in tool.Geometry.get_openings(element)]
|
||||
if not openings_elements:
|
||||
return
|
||||
@@ -531,15 +520,18 @@ class UpdateOpeningsFocus(Operator):
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
if preferences.opening_focus_opacity == 100:
|
||||
return {"FINISHED"}
|
||||
openings = set()
|
||||
building_objects = set()
|
||||
openings: set[bpy.types.Object] = set()
|
||||
building_objects: set[bpy.types.Object] = set()
|
||||
props = tool.Model.get_model_props()
|
||||
for opening in props.openings:
|
||||
if opening.obj:
|
||||
openings.add(opening.obj)
|
||||
opening_element = tool.Ifc.get_entity(opening.obj)
|
||||
assert opening_element
|
||||
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
|
||||
building_objects.add(tool.Ifc.get_object(building_element))
|
||||
building_obj = tool.Ifc.get_object(building_element)
|
||||
assert isinstance(building_obj, bpy.types.Object)
|
||||
building_objects.add(building_obj)
|
||||
|
||||
for obj in context.scene.objects:
|
||||
obj.color = [
|
||||
|
||||
@@ -599,6 +599,7 @@ class PolylineOperator:
|
||||
"Increment Angle": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_MMB_SCROLL"]},
|
||||
"Modify Snap Point": {"icons": True, "keys": ["EVENT_M"]},
|
||||
"Close Polyline": {"icons": True, "keys": ["EVENT_C"]},
|
||||
"Offset": {"icons": True, "keys": ["EVENT_O"]},
|
||||
"Remove Point": {"icons": True, "keys": ["EVENT_BACKSPACE"]},
|
||||
}
|
||||
|
||||
@@ -642,12 +643,22 @@ class PolylineOperator:
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
def choose_plane(self, event: bpy.types.Event, x: bool = True, y: bool = True, z: bool = True) -> None:
|
||||
def get_plane_origin():
|
||||
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) > 0:
|
||||
reference_point = polyline_points[-1]
|
||||
else:
|
||||
reference_point = Vector((0.0, 0.0, 0.0))
|
||||
self.tool_state.plane_origin = Vector((reference_point.x, reference_point.y, reference_point.z))
|
||||
|
||||
if x:
|
||||
if event.shift and event.value == "PRESS" and event.type == "X":
|
||||
self.tool_state.use_default_container = False
|
||||
self.tool_state.plane_method = "YZ" if self.tool_state.plane_method != "YZ" else None
|
||||
self.tool_state.axis_method = None
|
||||
tool.Blender.update_viewport()
|
||||
get_plane_origin()
|
||||
|
||||
if y:
|
||||
if event.shift and event.value == "PRESS" and event.type == "Y":
|
||||
@@ -655,6 +666,7 @@ class PolylineOperator:
|
||||
self.tool_state.plane_method = "XZ" if self.tool_state.plane_method != "XZ" else None
|
||||
self.tool_state.axis_method = None
|
||||
tool.Blender.update_viewport()
|
||||
get_plane_origin()
|
||||
|
||||
if z:
|
||||
if event.shift and event.value == "PRESS" and event.type == "Z":
|
||||
@@ -662,6 +674,7 @@ class PolylineOperator:
|
||||
self.tool_state.plane_method = "XY" if self.tool_state.plane_method != "XY" else None
|
||||
self.tool_state.axis_method = None
|
||||
tool.Blender.update_viewport()
|
||||
get_plane_origin()
|
||||
|
||||
def handle_instructions(
|
||||
self, context: bpy.types.Context, custom_instructions: dict = {}, custom_info: str = "", overwrite: bool = False
|
||||
@@ -879,6 +892,7 @@ class PolylineOperator:
|
||||
else:
|
||||
if event.value == "RELEASE" and event.type in {"ESC"}:
|
||||
self.tool_state.axis_method = None
|
||||
self.tool_state.plane_method = None
|
||||
context.workspace.status_text_set(text=None)
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Polyline.clear_polyline()
|
||||
|
||||
@@ -569,7 +569,7 @@ class AlignProduct(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
AlignType = Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]
|
||||
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
|
||||
align_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(AlignType)]
|
||||
)
|
||||
|
||||
@@ -577,49 +577,12 @@ class AlignProduct(bpy.types.Operator):
|
||||
align_type: AlignType
|
||||
|
||||
def execute(self, context):
|
||||
selected_objs = context.selected_objects
|
||||
if len(selected_objs) < 2 or not context.active_object:
|
||||
self.report({"ERROR"}, "Please select atleast 2 objects.")
|
||||
return {"FINISHED"}
|
||||
if self.align_type == "CENTERLINE":
|
||||
point = context.active_object.matrix_world @ (
|
||||
Vector(context.active_object.bound_box[0]) + (context.active_object.dimensions / 2)
|
||||
)
|
||||
elif self.align_type == "POSITIVE":
|
||||
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[6])
|
||||
elif self.align_type == "NEGATIVE":
|
||||
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[0])
|
||||
else:
|
||||
assert_never(self.align_type)
|
||||
|
||||
active_x_axis = context.active_object.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
active_y_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
active_z_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
|
||||
x_distances = self.get_axis_distances(point, active_x_axis, context)
|
||||
y_distances = self.get_axis_distances(point, active_y_axis, context)
|
||||
if abs(sum(x_distances)) < abs(sum(y_distances)):
|
||||
for i, obj in enumerate(selected_objs):
|
||||
obj.matrix_world = Matrix.Translation(active_x_axis * -x_distances[i]) @ obj.matrix_world
|
||||
else:
|
||||
for i, obj in enumerate(selected_objs):
|
||||
obj.matrix_world = Matrix.Translation(active_y_axis * -y_distances[i]) @ obj.matrix_world
|
||||
try:
|
||||
core.align_objects(tool.Blender, tool.Model, self.align_type)
|
||||
except core.RequireAtLeastTwoElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_axis_distances(self, point: Vector, axis: Vector, context: bpy.types.Context) -> list[float]:
|
||||
results = []
|
||||
for obj in context.selected_objects:
|
||||
if self.align_type == "CENTERLINE":
|
||||
obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2))
|
||||
elif self.align_type == "POSITIVE":
|
||||
obj_point = obj.matrix_world @ Vector(obj.bound_box[6])
|
||||
elif self.align_type == "NEGATIVE":
|
||||
obj_point = obj.matrix_world @ Vector(obj.bound_box[0])
|
||||
else:
|
||||
assert_never(self.align_type)
|
||||
results.append(mathutils.geometry.distance_point_to_plane(obj_point, point, axis))
|
||||
return results
|
||||
|
||||
|
||||
class LoadTypeThumbnails(bpy.types.Operator):
|
||||
bl_idname = "bim.load_type_thumbnails"
|
||||
@@ -645,8 +608,7 @@ class LoadTypeThumbnails(bpy.types.Operator):
|
||||
while queue:
|
||||
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
|
||||
element = queue.pop()
|
||||
if tool.Model.update_thumbnail_for_element(element):
|
||||
queue.append(element)
|
||||
tool.Model.mark_thumbnail_for_update(element)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import bonsai.core.root
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from math import pi, degrees, atan2
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.module.model.wall import DumbWallRecalculator
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
from typing import Union, Any, Optional
|
||||
@@ -213,7 +212,7 @@ class DumbProfileRegenerator:
|
||||
|
||||
# update related thumbnails
|
||||
for element in self.get_element_types_using_profile(profile):
|
||||
tool.Model.update_thumbnail_for_element(element, refresh=True)
|
||||
tool.Model.mark_thumbnail_for_update(element)
|
||||
|
||||
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
|
||||
self.file = ifc_file
|
||||
@@ -969,7 +968,7 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.matrix_world @= rotate_matrix
|
||||
bpy.context.view_layer.update()
|
||||
DumbProfileRecalculator().recalculate(profile_objs)
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1197,6 +1196,7 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
|
||||
):
|
||||
self.create_profiles_from_polyline(context)
|
||||
context.workspace.status_text_set(text=None)
|
||||
self.tool_state.plane_method = None
|
||||
ProductDecorator.uninstall()
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Polyline.clear_polyline()
|
||||
|
||||
@@ -221,13 +221,13 @@ class BIMModelProperties(PropertyGroup):
|
||||
items=[("EXTERIOR", "Exterior", ""), ("CENTER", "Center", ""), ("INTERIOR", "Interior", "")],
|
||||
name="Vertical Layer Offset Type",
|
||||
default="EXTERIOR",
|
||||
description="It's a convention that affects the offset to reference line",
|
||||
description="Offset convention to reference line",
|
||||
)
|
||||
offset_type_horizontal: bpy.props.EnumProperty(
|
||||
items=[("TOP", "Top", ""), ("CENTER", "Center", ""), ("BOTTOM", "Bottom", "")],
|
||||
name="Horizontal Layer Offset Type",
|
||||
default="TOP",
|
||||
description="It's a convention that affects the offset to reference line",
|
||||
description="Offset convention to reference line",
|
||||
)
|
||||
offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line")
|
||||
show_wall_axis: bpy.props.BoolProperty(
|
||||
|
||||
@@ -36,7 +36,6 @@ from math import cos, pi
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
from bonsai.bim.module.model.wall import DumbWallRecalculator
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -1035,5 +1034,5 @@ class RecalculateSlab(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement.is_a("IfcWall"):
|
||||
walls.append(tool.Ifc.get_object(rel.RelatedElement))
|
||||
|
||||
DumbWallRecalculator().recalculate(walls)
|
||||
tool.Model.recalculate_walls(walls)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -117,8 +117,8 @@ class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.geometry.connect_wall(
|
||||
tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True
|
||||
)
|
||||
joiner.recreate_wall(element, obj)
|
||||
joiner.recreate_wall(target_element, target_obj)
|
||||
tool.Model.recreate_wall(element, obj)
|
||||
tool.Model.recreate_wall(target_element, target_obj)
|
||||
else:
|
||||
self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element")
|
||||
|
||||
@@ -129,42 +129,22 @@ class AlignWall(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = """ Align the selected walls to the active wall:
|
||||
'Ext.': align to the EXTERIOR face
|
||||
'C/L': align to wall CENTERLINE
|
||||
'C/L': align to wall CENTER
|
||||
'Int.': align to the INTERIOR face"""
|
||||
|
||||
AlignType = Literal["CENTERLINE", "EXTERIOR", "INTERIOR"]
|
||||
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
|
||||
AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"]
|
||||
align_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
|
||||
items=((i, i, "") for i in get_args(AlignType))
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
align_type: AlignType
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object selected.")
|
||||
return False
|
||||
selected_valid_objects = tool.Model.get_selected_mesh_objects()
|
||||
if len(selected_valid_objects) < 2:
|
||||
cls.poll_message_set("Please select at least two mesh objects.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
selected_objects = tool.Model.get_selected_mesh_objects()
|
||||
for obj in selected_objects:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
aligner = DumbWallAligner(obj, context.active_object)
|
||||
if self.align_type == "CENTERLINE":
|
||||
aligner.align_centerline()
|
||||
elif self.align_type == "EXTERIOR":
|
||||
aligner.align_first_layer()
|
||||
elif self.align_type == "INTERIOR":
|
||||
aligner.align_last_layer()
|
||||
else:
|
||||
assert_never(self.align_type)
|
||||
try:
|
||||
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), self.align_type)
|
||||
except core.RequireAtLeastTwoLayeredElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -253,7 +233,7 @@ class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
objects = tool.Model.get_selected_mesh_ifc_objects()
|
||||
DumbWallRecalculator().recalculate(objects)
|
||||
tool.Model.recalculate_walls(objects)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -300,7 +280,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
layer2_objs.append(obj)
|
||||
|
||||
if layer2_objs:
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -413,7 +393,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
|
||||
if layer2_objs:
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -436,7 +416,24 @@ class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
for obj in selected_objs:
|
||||
joiner.set_length(obj, self.length)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OffsetWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.offset_walls"
|
||||
bl_label = "Offset Walls"
|
||||
bl_description = "Offset selected objects from their reference line."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Model.get_model_props()
|
||||
core.offset_walls(tool.Ifc, tool.Blender, tool.Model, props.offset_type_vertical)
|
||||
|
||||
|
||||
class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -507,12 +504,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
material_set_usage = model.by_id(material.id())
|
||||
# if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense}
|
||||
ifcopenshell.api.run(
|
||||
"material.edit_layer_usage",
|
||||
model,
|
||||
**{"usage": material_set_usage, "attributes": attributes},
|
||||
)
|
||||
DumbWallRecalculator().recalculate([wall["obj"]])
|
||||
ifcopenshell.api.run("material.edit_layer_usage", model, usage=material_set_usage, attributes=attributes)
|
||||
tool.Model.recalculate_walls([wall["obj"]])
|
||||
|
||||
if walls:
|
||||
if is_polyline_closed:
|
||||
@@ -578,6 +571,7 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
):
|
||||
self.create_walls_from_polyline(context)
|
||||
context.workspace.status_text_set(text=None)
|
||||
self.tool_state.plane_method = None
|
||||
ProductDecorator.uninstall()
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Polyline.clear_polyline()
|
||||
@@ -613,11 +607,11 @@ class DumbWallAligner:
|
||||
# An alignment shifts the origin of all walls to the closest point on the
|
||||
# local X axis of the reference wall. In addition, the Z rotation is copied.
|
||||
# Z translations are ignored for alignment.
|
||||
def __init__(self, wall: bpy.types.Object, reference_wall: bpy.types.Object):
|
||||
self.wall = wall
|
||||
def set_reference_wall(self, reference_wall: bpy.types.Object):
|
||||
self.reference_wall = reference_wall
|
||||
|
||||
def align_centerline(self) -> None:
|
||||
def align_centerline(self, wall: bpy.types.Object) -> None:
|
||||
self.wall = wall
|
||||
self.align_rotation()
|
||||
|
||||
l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5)
|
||||
@@ -635,7 +629,8 @@ class DumbWallAligner:
|
||||
new_origin = point - offset
|
||||
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
|
||||
|
||||
def align_last_layer(self) -> None:
|
||||
def align_last_layer(self, wall: bpy.types.Object) -> None:
|
||||
self.wall = wall
|
||||
self.align_rotation()
|
||||
|
||||
if self.is_rotation_flipped():
|
||||
@@ -658,7 +653,8 @@ class DumbWallAligner:
|
||||
new_origin = point - offset
|
||||
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
|
||||
|
||||
def align_first_layer(self) -> None:
|
||||
def align_first_layer(self, wall: bpy.types.Object) -> None:
|
||||
self.wall = wall
|
||||
self.align_rotation()
|
||||
|
||||
if self.is_rotation_flipped():
|
||||
@@ -700,30 +696,6 @@ class DumbWallAligner:
|
||||
return round(degrees(angle) % 360) == 180
|
||||
|
||||
|
||||
class DumbWallRecalculator:
|
||||
def recalculate(self, walls: list[bpy.types.Object]) -> None:
|
||||
queue: set[tuple[ifcopenshell.entity_instance, bpy.types.Object]] = set()
|
||||
for wall in walls:
|
||||
element = tool.Ifc.get_entity(wall)
|
||||
if tool.Ifc.is_moved(wall):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall)
|
||||
queue.add((element, wall))
|
||||
for rel in getattr(element, "ConnectedTo", []):
|
||||
obj = tool.Ifc.get_object(rel.RelatedElement)
|
||||
if tool.Ifc.is_moved(obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
queue.add((rel.RelatedElement, obj))
|
||||
for rel in getattr(element, "ConnectedFrom", []):
|
||||
obj = tool.Ifc.get_object(rel.RelatingElement)
|
||||
if tool.Ifc.is_moved(obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
queue.add((rel.RelatingElement, obj))
|
||||
joiner = DumbWallJoiner()
|
||||
for element, wall in queue:
|
||||
if tool.Model.get_usage_type(element) == "LAYER2" and wall:
|
||||
joiner.recreate_wall(element, wall)
|
||||
|
||||
|
||||
class DumbWallGenerator:
|
||||
def __init__(self, relating_type):
|
||||
self.relating_type = relating_type
|
||||
@@ -955,7 +927,7 @@ class DumbWallPlaner:
|
||||
else:
|
||||
for rel in inverse.AssociatedTo:
|
||||
walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects])
|
||||
DumbWallRecalculator().recalculate([w for w in set(walls) if w])
|
||||
tool.Model.recalculate_walls([w for w in set(walls) if w])
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
relating_type = settings["relating_type"]
|
||||
@@ -986,7 +958,7 @@ class DumbWallPlaner:
|
||||
if layer_set_direction:
|
||||
material.LayerSetDirection = layer_set_direction
|
||||
if material.LayerSetDirection == "AXIS2":
|
||||
DumbWallRecalculator().recalculate([obj])
|
||||
tool.Model.recalculate_walls([obj])
|
||||
|
||||
|
||||
class DumbWallJoiner:
|
||||
@@ -1006,7 +978,7 @@ class DumbWallJoiner:
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
axis = copy.deepcopy(axis1["reference"])
|
||||
body = copy.deepcopy(axis1["reference"])
|
||||
self.recreate_wall(element1, wall1, axis, body)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def split(self, wall1: bpy.types.Object, target: Vector) -> None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
@@ -1019,7 +991,6 @@ class DumbWallJoiner:
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
|
||||
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
axis2 = copy.deepcopy(axis1)
|
||||
intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
|
||||
if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)):
|
||||
return
|
||||
@@ -1080,20 +1051,36 @@ class DumbWallJoiner:
|
||||
# During the duplication process, filled voids are not copied. So we
|
||||
# only need to check fillings on the original element1.
|
||||
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings if r.RelatedOpeningElement.HasFillings]:
|
||||
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
|
||||
rel = opening.HasFillings[0]
|
||||
filling = rel.RelatedBuildingElement
|
||||
filling_obj = tool.Ifc.get_object(filling)
|
||||
filling_location = filling_obj.matrix_world.translation
|
||||
_, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis1["reference"])
|
||||
if filling_position > cut_percentage:
|
||||
# The filling should be moved from element1 to element2.
|
||||
FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation)
|
||||
new_opening = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=opening)
|
||||
new_opening.VoidsElements[0].RelatingBuildingElement = element2
|
||||
if new_opening.ObjectPlacement and new_opening.ObjectPlacement.is_a("IfcLocalPlacement"):
|
||||
if element2.ObjectPlacement:
|
||||
new_opening.ObjectPlacement.PlacementRelTo = element2.ObjectPlacement
|
||||
# For now, we do copy opening representations
|
||||
if opening.Representation:
|
||||
new_opening.Representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(), opening.Representation, exclude=["IfcGeometricRepresentationContext"]
|
||||
)
|
||||
|
||||
rel.RelatedBuildingElement = element2
|
||||
|
||||
# Remove the old opening
|
||||
ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
|
||||
|
||||
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
|
||||
p3 = (wall1.matrix_world.inverted() @ intersect.to_3d()).to_2d() / unit_scale
|
||||
self.set_axis(element1, p1, p3)
|
||||
self.set_axis(element2, p3, p2)
|
||||
|
||||
self.recreate_wall(element1, wall1)
|
||||
self.recreate_wall(element2, wall2)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element2, wall2)
|
||||
|
||||
def flip(self, wall1: bpy.types.Object) -> None:
|
||||
if tool.Ifc.is_moved(wall1):
|
||||
@@ -1120,7 +1107,7 @@ class DumbWallJoiner:
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False
|
||||
)
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def merge(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
||||
if tool.Ifc.is_moved(wall1):
|
||||
@@ -1141,7 +1128,7 @@ class DumbWallJoiner:
|
||||
p3 = (matrix1i @ matrix2 @ np.concatenate((p3, (0, 1))))[:2]
|
||||
p4 = (matrix1i @ matrix2 @ np.concatenate((p4, (0, 1))))[:2]
|
||||
|
||||
if not np.isclose(p1[1], p4[1]) or not np.isclose(p3[1], p4[1]):
|
||||
if not np.isclose(p1[1], p4[1], atol=1e-02) or not np.isclose(p3[1], p4[1], atol=1e-02):
|
||||
return
|
||||
|
||||
x_ordinates = tuple(co[0] for co in (p1, p2, p3, p4))
|
||||
@@ -1173,7 +1160,7 @@ class DumbWallJoiner:
|
||||
related_connection=rel.RelatedConnectionType,
|
||||
)
|
||||
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
tool.Geometry.delete_ifc_object(wall2)
|
||||
|
||||
@@ -1206,7 +1193,7 @@ class DumbWallJoiner:
|
||||
description="TOP",
|
||||
)
|
||||
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def set_axis(self, wall, p1, p2):
|
||||
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
||||
@@ -1235,7 +1222,7 @@ class DumbWallJoiner:
|
||||
self.set_axis(element1, p1, intersect)
|
||||
else:
|
||||
self.set_axis(element1, intersect, p2)
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def set_length(self, wall1: bpy.types.Object, si_length: float) -> None:
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
@@ -1249,7 +1236,7 @@ class DumbWallJoiner:
|
||||
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
|
||||
p2[0] = p1[0] + si_length / unit_scale
|
||||
self.set_axis(element1, p1, p2)
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
@@ -1272,7 +1259,7 @@ class DumbWallJoiner:
|
||||
description="BUTT",
|
||||
)
|
||||
|
||||
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
||||
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
||||
|
||||
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
|
||||
wall1 = tool.Ifc.get_entity(obj1)
|
||||
@@ -1282,27 +1269,8 @@ class DumbWallJoiner:
|
||||
if tool.Ifc.is_moved(obj2):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj2)
|
||||
ifcopenshell.api.geometry.connect_wall(tool.Ifc.get(), wall1=wall1, wall2=wall2)
|
||||
self.recreate_wall(wall1, obj1)
|
||||
self.recreate_wall(wall2, obj2)
|
||||
|
||||
def recreate_wall(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None) -> None:
|
||||
rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=rep,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
matrix[:, 3] *= unit_scale
|
||||
obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix)
|
||||
tool.Geometry.record_object_position(obj)
|
||||
tool.Model.recreate_wall(wall1, obj1)
|
||||
tool.Model.recreate_wall(wall2, obj2)
|
||||
|
||||
def create_matrix(self, p, x, y, z):
|
||||
return Matrix([x, y, z, p]).to_4x4().transposed()
|
||||
|
||||
@@ -529,6 +529,9 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_editing = False
|
||||
|
||||
update_window_modifier_representation(context)
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type:
|
||||
tool.Model.mark_thumbnail_for_update(element_type)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
|
||||
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
|
||||
|
||||
@@ -23,7 +23,7 @@ import bpy.utils.previews
|
||||
import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.model as core
|
||||
from bonsai.bim.module.model.wall import DumbWallJoiner
|
||||
from bonsai.bim.module.model.wall import DumbWallJoiner, DumbWallAligner
|
||||
from bonsai.bim.helper import prop_with_search, draw_attribute
|
||||
from bpy.types import WorkSpaceTool, Menu
|
||||
from bonsai.bim.module.model.data import AuthoringData, ItemData
|
||||
@@ -773,6 +773,12 @@ class EditObjectUI:
|
||||
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
|
||||
op.x_angle = cls.props.x_angle
|
||||
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
row.prop(
|
||||
data=cls.props, property="offset_type_vertical", text="Offset" if ui_context != "TOOL_HEADER" else ""
|
||||
)
|
||||
row.operator("bim.offset_walls", icon="FILE_REFRESH", text="")
|
||||
|
||||
elif AuthoringData.data["active_material_usage"] == "LAYER3":
|
||||
row.prop(data=cls.props, property="x_angle", text="Angle" if ui_context != "TOOL_HEADER" else "A")
|
||||
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
|
||||
@@ -1181,10 +1187,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
if self.active_material_usage == "LAYER2":
|
||||
if bpy.ops.bim.align_wall.poll():
|
||||
bpy.ops.bim.align_wall(align_type="CENTERLINE")
|
||||
try:
|
||||
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "CENTER")
|
||||
except core.RequireAtLeastTwoLayeredElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
else:
|
||||
bpy.ops.bim.align_product(align_type="CENTERLINE")
|
||||
try:
|
||||
core.align_objects(tool.Blender, tool.Model, "CENTER")
|
||||
except core.RequireAtLeastTwoElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
|
||||
def hotkey_S_E(self):
|
||||
if not bpy.context.selected_objects or not (active_object := bpy.context.active_object):
|
||||
@@ -1328,18 +1339,29 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
elif self.active_material_usage == "LAYER2":
|
||||
bpy.ops.bim.align_wall(align_type="INTERIOR")
|
||||
try:
|
||||
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "INTERIOR")
|
||||
except core.RequireAtLeastTwoLayeredElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
else:
|
||||
bpy.ops.bim.align_product(align_type="POSITIVE")
|
||||
try:
|
||||
core.align_objects(tool.Blender, tool.Model, "POSITIVE")
|
||||
except core.RequireAtLeastTwoElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
|
||||
def hotkey_S_X(self):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
if self.active_material_usage == "LAYER2":
|
||||
if bpy.ops.bim.align_wall.poll():
|
||||
bpy.ops.bim.align_wall(align_type="EXTERIOR")
|
||||
try:
|
||||
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "EXTERIOR")
|
||||
except core.RequireAtLeastTwoLayeredElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
else:
|
||||
bpy.ops.bim.align_product(align_type="NEGATIVE")
|
||||
try:
|
||||
core.align_objects(tool.Blender, tool.Model, "NEGATIVE")
|
||||
except core.RequireAtLeastTwoElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
|
||||
def hotkey_S_Y(self):
|
||||
if not bpy.context.selected_objects:
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
from typing import Any, Union
|
||||
from ifcopenshell.util.doc import get_entity_doc
|
||||
|
||||
|
||||
@@ -31,42 +33,46 @@ def refresh():
|
||||
|
||||
class RolesAddressesData:
|
||||
@classmethod
|
||||
def get_roles(cls, parent):
|
||||
results = []
|
||||
def get_roles(cls, parent: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
|
||||
props = tool.Owner.get_owner_props()
|
||||
results: list[dict[str, Any]] = []
|
||||
for role in parent.Roles or []:
|
||||
results.append(
|
||||
{
|
||||
"id": role.id(),
|
||||
"is_editing": bpy.context.scene.BIMOwnerProperties.active_role_id == role.id(),
|
||||
"is_editing": props.active_role_id == role.id(),
|
||||
"label": role.UserDefinedRole or role.Role,
|
||||
"props": bpy.context.scene.BIMOwnerProperties.role_attributes,
|
||||
"props": props.role_attributes,
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def get_addresses(cls, parent):
|
||||
results = []
|
||||
def get_addresses(cls, parent: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
|
||||
props = tool.Owner.get_owner_props()
|
||||
results: list[dict[str, Any]] = []
|
||||
for address in parent.Addresses or []:
|
||||
results.append(
|
||||
{
|
||||
"id": address.id(),
|
||||
"is_editing": bpy.context.scene.BIMOwnerProperties.active_address_id == address.id(),
|
||||
"is_editing": props.active_address_id == address.id(),
|
||||
"label": address.is_a(),
|
||||
"props": bpy.context.scene.BIMOwnerProperties.address_attributes,
|
||||
"props": props.address_attributes,
|
||||
"list_attributes": cls.get_address_list_attributes(address),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def get_address_list_attributes(cls, address):
|
||||
results = []
|
||||
props = bpy.context.scene.BIMOwnerProperties
|
||||
def get_address_list_attributes(cls, address: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
props = tool.Owner.get_owner_props()
|
||||
if address.is_a("IfcPostalAddress"):
|
||||
names = ["AddressLines"]
|
||||
elif address.is_a("IfcTelecomAddress"):
|
||||
names = ["TelephoneNumbers", "FacsimileNumbers", "ElectronicMailAddresses", "MessagingIDs"]
|
||||
else:
|
||||
assert False, f"Unexpected entity: {address}"
|
||||
for name in names:
|
||||
if name == "AddressLines":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.address_lines)]
|
||||
@@ -78,6 +84,8 @@ class RolesAddressesData:
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.electronic_mail_addresses)]
|
||||
elif name == "MessagingIDs":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.messaging_ids)]
|
||||
else:
|
||||
assert False, f"Unexpected name: {name}"
|
||||
results.append({"name": name, "items": items})
|
||||
return results
|
||||
|
||||
@@ -92,14 +100,15 @@ class PeopleData(RolesAddressesData):
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def get_people(cls):
|
||||
people = []
|
||||
def get_people(cls) -> list[dict[str, Any]]:
|
||||
props = tool.Owner.get_owner_props()
|
||||
people: list[dict[str, Any]] = []
|
||||
for person in tool.Ifc.get().by_type("IfcPerson"):
|
||||
roles = cls.get_roles(person)
|
||||
people.append(
|
||||
{
|
||||
"id": person.id(),
|
||||
"props": bpy.context.scene.BIMOwnerProperties.person_attributes,
|
||||
"props": props.person_attributes,
|
||||
"name": cls.get_person_name(person),
|
||||
"roles_label": ", ".join([r["label"] for r in roles]),
|
||||
"is_editing": cls.get_person_is_editing(person),
|
||||
@@ -112,7 +121,7 @@ class PeopleData(RolesAddressesData):
|
||||
return people
|
||||
|
||||
@classmethod
|
||||
def get_person_name(cls, person):
|
||||
def get_person_name(cls, person: ifcopenshell.entity_instance) -> str:
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
name = person.Id
|
||||
else:
|
||||
@@ -124,20 +133,24 @@ class PeopleData(RolesAddressesData):
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def get_person_is_editing(cls, person):
|
||||
return bpy.context.scene.BIMOwnerProperties.active_person_id == person.id()
|
||||
def get_person_is_editing(cls, person: ifcopenshell.entity_instance) -> bool:
|
||||
props = tool.Owner.get_owner_props()
|
||||
return props.active_person_id == person.id()
|
||||
|
||||
@classmethod
|
||||
def get_person_list_attributes(cls, person):
|
||||
results = []
|
||||
props = bpy.context.scene.BIMOwnerProperties
|
||||
for name in ["MiddleNames", "PrefixTitles", "SuffixTitles"]:
|
||||
def get_person_list_attributes(cls, person: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
props = tool.Owner.get_owner_props()
|
||||
name: tool.Owner.PersonAttributeType
|
||||
for name in ("MiddleNames", "PrefixTitles", "SuffixTitles"):
|
||||
if name == "MiddleNames":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.middle_names)]
|
||||
elif name == "PrefixTitles":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.prefix_titles)]
|
||||
elif name == "SuffixTitles":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.suffix_titles)]
|
||||
else:
|
||||
assert False, name
|
||||
results.append({"name": name, "items": items})
|
||||
return results
|
||||
|
||||
@@ -152,17 +165,18 @@ class OrganisationsData(RolesAddressesData):
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def get_organisations(cls):
|
||||
organisations = []
|
||||
def get_organisations(cls) -> list[dict[str, Any]]:
|
||||
props = tool.Owner.get_owner_props()
|
||||
organisations: list[dict[str, Any]] = []
|
||||
for organisation in tool.Ifc.get().by_type("IfcOrganization"):
|
||||
roles = cls.get_roles(organisation)
|
||||
organisations.append(
|
||||
{
|
||||
"id": organisation.id(),
|
||||
"props": bpy.context.scene.BIMOwnerProperties.organisation_attributes,
|
||||
"props": props.organisation_attributes,
|
||||
"name": organisation.Name,
|
||||
"roles_label": ", ".join([r["label"] for r in roles]),
|
||||
"is_editing": bpy.context.scene.BIMOwnerProperties.active_organisation_id == organisation.id(),
|
||||
"is_editing": props.active_organisation_id == organisation.id(),
|
||||
"is_engaged": bool(organisation.Engages),
|
||||
"roles": roles,
|
||||
"addresses": cls.get_addresses(organisation),
|
||||
@@ -186,26 +200,27 @@ class OwnerData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def get_user_person(cls):
|
||||
def get_user_person(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcPerson")]
|
||||
|
||||
@classmethod
|
||||
def get_user_organisation(cls):
|
||||
def get_user_organisation(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcOrganization")]
|
||||
|
||||
@classmethod
|
||||
def can_add_user(cls):
|
||||
return tool.Ifc.get().by_type("IfcPerson") and tool.Ifc.get().by_type("IfcOrganization")
|
||||
def can_add_user(cls) -> bool:
|
||||
return bool(tool.Ifc.get().by_type("IfcPerson") and tool.Ifc.get().by_type("IfcOrganization"))
|
||||
|
||||
@classmethod
|
||||
def get_users(cls):
|
||||
results = []
|
||||
def get_users(cls) -> list[dict[str, Any]]:
|
||||
props = tool.Owner.get_owner_props()
|
||||
results: list[dict[str, Any]] = []
|
||||
for user in tool.Ifc.get().by_type("IfcPersonAndOrganization"):
|
||||
results.append(
|
||||
{
|
||||
"id": user.id(),
|
||||
"label": "{} ({})".format(user.ThePerson[0] or "Unnamed", user.TheOrganization[0] or "Unnamed"),
|
||||
"is_active": bpy.context.scene.BIMOwnerProperties.active_user_id == user.id(),
|
||||
"is_active": props.active_user_id == user.id(),
|
||||
}
|
||||
)
|
||||
return results
|
||||
@@ -224,48 +239,60 @@ class ActorData:
|
||||
cls.data["actors"] = cls.actors()
|
||||
|
||||
@classmethod
|
||||
def the_actor(cls):
|
||||
if not (ifc_class := bpy.context.scene.BIMOwnerProperties.actor_type):
|
||||
def the_actor(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
props = tool.Owner.get_owner_props()
|
||||
if not (ifc_class := props.actor_type):
|
||||
ifc_class = cls.actor_type()[0][0]
|
||||
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type(ifc_class)]
|
||||
|
||||
@classmethod
|
||||
def actors(cls):
|
||||
actors = []
|
||||
props = bpy.context.scene.BIMOwnerProperties
|
||||
def actors(cls) -> list[dict[str, Any]]:
|
||||
actors: list[dict[str, Any]] = []
|
||||
props = tool.Owner.get_owner_props()
|
||||
for actor in tool.Ifc.get().by_type(props.actor_class, include_subtypes=False):
|
||||
is_editing = props.active_actor_id == actor.id()
|
||||
if actor.TheActor.is_a("IfcPerson"):
|
||||
the_actor = actor.TheActor.Identification or "N/A"
|
||||
elif actor.TheActor.is_a("IfcOrganization"):
|
||||
the_actor = actor.TheActor.Identification or "N/A"
|
||||
elif actor.TheActor.is_a("IfcPersonAndOrganization"):
|
||||
the_actor = actor.TheActor.ThePerson.Identification or "N/A"
|
||||
the_actor += "-" + actor.TheActor.TheOrganization.Identification or "N/A"
|
||||
the_actor: ifcopenshell.entity_instance = actor.TheActor
|
||||
if the_actor.is_a("IfcPerson"):
|
||||
the_actor_ = the_actor.Identification or "N/A"
|
||||
elif the_actor.is_a("IfcOrganization"):
|
||||
the_actor_ = the_actor.Identification or "N/A"
|
||||
elif the_actor.is_a("IfcPersonAndOrganization"):
|
||||
the_actor_ = the_actor.ThePerson.Identification or "N/A"
|
||||
the_actor_ += "-" + the_actor.TheOrganization.Identification or "N/A"
|
||||
else:
|
||||
assert False, the_actor
|
||||
actors.append(
|
||||
{"id": actor.id(), "name": actor.Name or "Unnamed", "the_actor": the_actor, "is_editing": is_editing}
|
||||
{
|
||||
"id": actor.id(),
|
||||
"name": actor.Name or "Unnamed",
|
||||
"the_actor": the_actor_,
|
||||
"is_editing": is_editing,
|
||||
}
|
||||
)
|
||||
return actors
|
||||
|
||||
@classmethod
|
||||
def actor_class(cls) -> list[tuple[str, str, str]]:
|
||||
def actor_class(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
version = tool.Ifc.get_schema()
|
||||
actor_doc = get_entity_doc(version, "IfcActor")
|
||||
occupant_doc = get_entity_doc(version, "IfcOccupant")
|
||||
assert actor_doc and occupant_doc
|
||||
return [
|
||||
("IfcActor", "Actor", get_entity_doc(version, "IfcActor").get("description", "")),
|
||||
("IfcOccupant", "Occupant", get_entity_doc(version, "IfcOccupant").get("description", "")),
|
||||
("IfcActor", "Actor", actor_doc.get("description", "")),
|
||||
("IfcOccupant", "Occupant", occupant_doc.get("description", "")),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def actor_type(cls) -> list[tuple[str, str, str]]:
|
||||
def actor_type(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
version = tool.Ifc.get_schema()
|
||||
person_doc = get_entity_doc(version, "IfcPerson")
|
||||
organization_doc = get_entity_doc(version, "IfcOrganization")
|
||||
pao_doc = get_entity_doc(version, "IfcPersonAndOrganization")
|
||||
assert person_doc and organization_doc and pao_doc
|
||||
return [
|
||||
("IfcPerson", "Person", get_entity_doc(version, "IfcPerson").get("description", "")),
|
||||
("IfcOrganization", "Organisation", get_entity_doc(version, "IfcOrganization").get("description", "")),
|
||||
(
|
||||
"IfcPersonAndOrganization",
|
||||
"User",
|
||||
get_entity_doc(version, "IfcPersonAndOrganization").get("description", ""),
|
||||
),
|
||||
("IfcPerson", "Person", person_doc.get("description", "")),
|
||||
("IfcOrganization", "Organisation", organization_doc.get("description", "")),
|
||||
("IfcPersonAndOrganization", "User", pao_doc.get("description", "")),
|
||||
]
|
||||
|
||||
|
||||
@@ -279,25 +306,31 @@ class ObjectActorData:
|
||||
cls.data = {"actor": cls.actor(), "actors": cls.actors()}
|
||||
|
||||
@classmethod
|
||||
def actor(cls):
|
||||
def actor(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
return [(str(p.id()), p.Name or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcActor")]
|
||||
|
||||
@classmethod
|
||||
def actors(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
def actors(cls) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return results
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return results
|
||||
for rel in getattr(element, "HasAssignments", []):
|
||||
if rel.is_a("IfcRelAssignsToActor"):
|
||||
actor = rel.RelatingActor
|
||||
if actor.TheActor.is_a("IfcPerson"):
|
||||
roles = cls.get_roles(actor.TheActor)
|
||||
elif actor.TheActor.is_a("IfcOrganization"):
|
||||
roles = cls.get_roles(actor.TheActor)
|
||||
elif actor.TheActor.is_a("IfcPersonAndOrganization"):
|
||||
roles = cls.get_roles(actor.TheActor.ThePerson)
|
||||
roles.extend(cls.get_roles(actor.TheActor.TheOrganization))
|
||||
the_actor: ifcopenshell.entity_instance = actor.TheActor
|
||||
if the_actor.is_a("IfcPerson"):
|
||||
roles = cls.get_roles(the_actor)
|
||||
elif the_actor.is_a("IfcOrganization"):
|
||||
roles = cls.get_roles(the_actor)
|
||||
elif the_actor.is_a("IfcPersonAndOrganization"):
|
||||
roles = cls.get_roles(the_actor.ThePerson)
|
||||
roles.extend(cls.get_roles(the_actor.TheOrganization))
|
||||
else:
|
||||
assert False, the_actor
|
||||
role = ", ".join(roles)
|
||||
results.append(
|
||||
{"id": actor.id(), "name": actor.Name or "Unnamed", "role": role, "ifc_class": actor.is_a()}
|
||||
@@ -305,5 +338,5 @@ class ObjectActorData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def get_roles(cls, parent):
|
||||
def get_roles(cls, parent: ifcopenshell.entity_instance) -> list[Union[str, None]]:
|
||||
return [r.UserDefinedRole or r.Role for r in parent.Roles or []]
|
||||
|
||||
@@ -19,25 +19,35 @@
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.owner as core
|
||||
from ifcopenshell.api.owner.add_address import ADDRESS_TYPE
|
||||
from typing import TYPE_CHECKING, get_args
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy._typing.rna_enums as rna_enums
|
||||
|
||||
|
||||
class EnableEditingPerson(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingPerson(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_person"
|
||||
bl_label = "Enable Editing Person"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
person: bpy.props.IntProperty()
|
||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
person: int
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.enable_editing_person(tool.Owner, person=tool.Ifc.get().by_id(self.person))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingPerson(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class DisableEditingPerson(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_person"
|
||||
bl_label = "Disable Editing Person"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.disable_editing_person(tool.Owner)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddPerson(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -45,7 +55,7 @@ class AddPerson(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Add Person"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context) -> None:
|
||||
core.add_person(tool.Ifc)
|
||||
|
||||
|
||||
@@ -62,57 +72,81 @@ class RemovePerson(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_person"
|
||||
bl_label = "Remove Person"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
person: bpy.props.IntProperty()
|
||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
person: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_person(tool.Ifc, person=tool.Ifc.get().by_id(self.person))
|
||||
|
||||
|
||||
class AddPersonAttribute(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddPersonAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.add_person_attribute"
|
||||
bl_label = "Add Person Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.add_person_attribute(tool.Owner, name=self.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemovePersonAttribute(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class RemovePersonAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_person_attribute"
|
||||
bl_label = "Remove Person Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
id: bpy.props.IntProperty()
|
||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
|
||||
)
|
||||
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
id: int
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.remove_person_attribute(tool.Owner, name=self.name, id=self.id)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingRole(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingRole(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_role"
|
||||
bl_label = "Enable Editing Role"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
role: bpy.props.IntProperty()
|
||||
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
role: int
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.enable_editing_role(tool.Owner, role=tool.Ifc.get().by_id(self.role))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingRole(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class DisableEditingRole(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_role"
|
||||
bl_label = "Disable Editing Role"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.disable_editing_role(tool.Owner)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddRole(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_role"
|
||||
bl_label = "Add Role"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.IntProperty()
|
||||
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
parent: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.add_role(tool.Ifc, parent=tool.Ifc.get().by_id(self.parent))
|
||||
@@ -131,7 +165,10 @@ class RemoveRole(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_role"
|
||||
bl_label = "Remove Role"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
role: bpy.props.IntProperty()
|
||||
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
role: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_role(tool.Ifc, role=tool.Ifc.get().by_id(self.role))
|
||||
@@ -141,51 +178,75 @@ class AddAddress(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_address"
|
||||
bl_label = "Add Address"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.IntProperty()
|
||||
ifc_class: bpy.props.StringProperty()
|
||||
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
ifc_class: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(ADDRESS_TYPE)),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
parent: int
|
||||
ifc_class: ADDRESS_TYPE
|
||||
|
||||
def _execute(self, context):
|
||||
core.add_address(tool.Ifc, parent=tool.Ifc.get().by_id(self.parent), ifc_class=self.ifc_class)
|
||||
|
||||
|
||||
class AddAddressAttribute(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddAddressAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.add_address_attribute"
|
||||
bl_label = "Add Address Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.add_address_attribute(tool.Owner, name=self.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveAddressAttribute(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class RemoveAddressAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_address_attribute"
|
||||
bl_label = "Remove Address Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
id: bpy.props.IntProperty()
|
||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
|
||||
)
|
||||
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
id: int
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.remove_address_attribute(tool.Owner, name=self.name, id=self.id)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingAddress(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingAddress(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_address"
|
||||
bl_label = "Enable Editing Address"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
address: bpy.props.IntProperty()
|
||||
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
address: int
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.enable_editing_address(tool.Owner, address=tool.Ifc.get().by_id(self.address))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingAddress(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class DisableEditingAddress(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_address"
|
||||
bl_label = "Disable Editing Address"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.disable_editing_address(tool.Owner)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditAddress(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -201,29 +262,37 @@ class RemoveAddress(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_address"
|
||||
bl_label = "Remove Address"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
address: bpy.props.IntProperty()
|
||||
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
address: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_address(tool.Ifc, address=tool.Ifc.get().by_id(self.address))
|
||||
|
||||
|
||||
class EnableEditingOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingOrganisation(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_organisation"
|
||||
bl_label = "Enable Editing Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
organisation: bpy.props.IntProperty()
|
||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
organisation: int
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.enable_editing_organisation(tool.Owner, organisation=tool.Ifc.get().by_id(self.organisation))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class DisableEditingOrganisation(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_organisation"
|
||||
bl_label = "Disable Editing Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.disable_editing_organisation(tool.Owner)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -248,7 +317,10 @@ class RemoveOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_organisation"
|
||||
bl_label = "Remove Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
organisation: bpy.props.IntProperty()
|
||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
organisation: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_organisation(tool.Ifc, tool.Ifc.get().by_id(self.organisation))
|
||||
@@ -258,8 +330,12 @@ class AddPersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_person_and_organisation"
|
||||
bl_label = "Add Person And Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
person: bpy.props.IntProperty()
|
||||
organisation: bpy.props.IntProperty()
|
||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
person: int
|
||||
organisation: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.add_person_and_organisation(
|
||||
@@ -271,7 +347,10 @@ class RemovePersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_person_and_organisation"
|
||||
bl_label = "Remove Person And Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
person_and_organisation: bpy.props.IntProperty()
|
||||
person_and_organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
person_and_organisation: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_person_and_organisation(
|
||||
@@ -279,24 +358,29 @@ class RemovePersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
class SetUser(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class SetUser(bpy.types.Operator):
|
||||
bl_idname = "bim.set_user"
|
||||
bl_label = "Set User"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
user: bpy.props.IntProperty()
|
||||
user: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
user: int
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.set_user(tool.Owner, user=tool.Ifc.get().by_id(self.user))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ClearUser(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class ClearUser(bpy.types.Operator):
|
||||
bl_idname = "bim.clear_user"
|
||||
bl_label = "Clear User"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
user: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.clear_user(tool.Owner)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -305,28 +389,33 @@ class AddActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMOwnerProperties
|
||||
props = tool.Owner.get_owner_props()
|
||||
if props.the_actor:
|
||||
core.add_actor(tool.Ifc, ifc_class=props.actor_class, actor=tool.Ifc.get().by_id(int(props.the_actor)))
|
||||
|
||||
|
||||
class EnableEditingActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingActor(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_actor"
|
||||
bl_label = "Enable Editing Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
actor: bpy.props.IntProperty()
|
||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def _execute(self, context):
|
||||
if TYPE_CHECKING:
|
||||
actor: int
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.enable_editing_actor(tool.Owner, actor=tool.Ifc.get().by_id(self.actor))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class DisableEditingActor(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_actor"
|
||||
bl_label = "Disable Editing Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
core.disable_editing_actor(tool.Owner)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -342,7 +431,10 @@ class RemoveActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_actor"
|
||||
bl_label = "Remove Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
actor: bpy.props.IntProperty()
|
||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
actor: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor))
|
||||
@@ -352,21 +444,27 @@ class AssignActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_actor"
|
||||
bl_label = "Assign Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
actor: bpy.props.IntProperty()
|
||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
actor: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.assign_actor(
|
||||
tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=tool.Ifc.get_entity(context.active_object)
|
||||
)
|
||||
assert (obj := context.active_object)
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
core.assign_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=element)
|
||||
|
||||
|
||||
class UnassignActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.unassign_actor"
|
||||
bl_label = "Unassign Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
actor: bpy.props.IntProperty()
|
||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
actor: int
|
||||
|
||||
def _execute(self, context):
|
||||
core.unassign_actor(
|
||||
tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=tool.Ifc.get_entity(context.active_object)
|
||||
)
|
||||
assert (obj := context.active_object)
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
core.unassign_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=element)
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.prop import StrProperty, Attribute
|
||||
from bonsai.bim.module.owner.data import OwnerData, ActorData, ObjectActorData
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -33,45 +34,45 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def get_user_person(self, context):
|
||||
def get_user_person(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not OwnerData.is_loaded:
|
||||
OwnerData.load()
|
||||
return OwnerData.data["user_person"]
|
||||
|
||||
|
||||
def get_user_organisation(self, context):
|
||||
def get_user_organisation(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not OwnerData.is_loaded:
|
||||
OwnerData.load()
|
||||
return OwnerData.data["user_organisation"]
|
||||
|
||||
|
||||
def get_the_actor(self, context):
|
||||
def get_the_actor(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not ActorData.is_loaded:
|
||||
ActorData.load()
|
||||
return ActorData.data["the_actor"]
|
||||
|
||||
|
||||
def get_actor(self, context):
|
||||
def get_actor(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not ObjectActorData.is_loaded:
|
||||
ObjectActorData.load()
|
||||
return ObjectActorData.data["actor"]
|
||||
|
||||
|
||||
def update_actor_type(self, context):
|
||||
def update_actor_type(self: "BIMOwnerProperties", context: bpy.types.Context) -> None:
|
||||
ActorData.data["the_actor"] = ActorData.the_actor()
|
||||
|
||||
|
||||
def update_actor_class(self, context):
|
||||
def update_actor_class(self: "BIMOwnerProperties", context: bpy.types.Context) -> None:
|
||||
ActorData.data["actors"] = ActorData.actors()
|
||||
|
||||
|
||||
def get_actor_class(self, context):
|
||||
def get_actor_class(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not ActorData.is_loaded:
|
||||
ActorData.load()
|
||||
return ActorData.data["actor_class"]
|
||||
|
||||
|
||||
def get_actor_type(self, context):
|
||||
def get_actor_type(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not ActorData.is_loaded:
|
||||
ActorData.load()
|
||||
return ActorData.data["actor_type"]
|
||||
@@ -119,3 +120,30 @@ class BIMOwnerProperties(PropertyGroup):
|
||||
items=get_the_actor, name="Actor", description="This entity represents an individual human being."
|
||||
)
|
||||
actor: EnumProperty(items=get_actor, name="Actor")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
active_person_id: int
|
||||
person_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
middle_names: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
prefix_titles: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
suffix_titles: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
active_organisation_id: int
|
||||
organisation_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
active_role_id: int
|
||||
role_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
active_address_id: int
|
||||
address_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
address_lines: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
telephone_numbers: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
facsimile_numbers: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
electronic_mail_addresses: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
messaging_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
user_person: str
|
||||
user_organisation: str
|
||||
active_user_id: int
|
||||
active_actor_id: int
|
||||
actor_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
actor_class: Literal["IfcActor", "IfcOccupant"]
|
||||
actor_type: str
|
||||
the_actor: str
|
||||
actor: str
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
import bpy
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
from typing import Any
|
||||
from bonsai.bim.module.owner.data import PeopleData, OrganisationsData, OwnerData, ActorData, ObjectActorData
|
||||
|
||||
|
||||
def draw_roles(box, parent):
|
||||
def draw_roles(box: bpy.types.UILayout, parent: dict[str, Any]) -> None:
|
||||
row = box.row(align=True)
|
||||
row.label(text="Roles")
|
||||
op = row.operator("bim.add_role", icon="ADD", text="")
|
||||
@@ -41,7 +42,7 @@ def draw_roles(box, parent):
|
||||
row.operator("bim.remove_role", icon="X", text="").role = role["id"]
|
||||
|
||||
|
||||
def draw_addresses(box, parent):
|
||||
def draw_addresses(box: bpy.types.UILayout, parent: dict[str, Any]) -> None:
|
||||
row = box.row(align=True)
|
||||
row.label(text="Addresses")
|
||||
op = row.operator("bim.add_address", icon="LINK_BLEND", text="")
|
||||
@@ -87,12 +88,13 @@ class BIM_PT_people(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
def draw(self, context):
|
||||
if not PeopleData.is_loaded:
|
||||
PeopleData.load()
|
||||
|
||||
assert self.layout
|
||||
self.layout.use_property_split = True
|
||||
self.layout.use_property_decorate = False
|
||||
|
||||
@@ -102,7 +104,8 @@ class BIM_PT_people(bpy.types.Panel):
|
||||
for person in PeopleData.data["people"]:
|
||||
self.draw_person(person)
|
||||
|
||||
def draw_person(self, person):
|
||||
def draw_person(self, person: dict[str, Any]) -> None:
|
||||
assert self.layout
|
||||
if person["is_editing"]:
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
@@ -146,12 +149,13 @@ class BIM_PT_organisations(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
def draw(self, context):
|
||||
if not OrganisationsData.is_loaded:
|
||||
OrganisationsData.load()
|
||||
|
||||
assert self.layout
|
||||
self.layout.use_property_split = True
|
||||
self.layout.use_property_decorate = False
|
||||
|
||||
@@ -161,7 +165,8 @@ class BIM_PT_organisations(bpy.types.Panel):
|
||||
for organisation in OrganisationsData.data["organisations"]:
|
||||
self.draw_organisation(organisation)
|
||||
|
||||
def draw_organisation(self, organisation):
|
||||
def draw_organisation(self, organisation: dict[str, Any]) -> None:
|
||||
assert self.layout
|
||||
if organisation["is_editing"]:
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
@@ -193,15 +198,16 @@ class BIM_PT_owner(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
def draw(self, context):
|
||||
if not OwnerData.is_loaded:
|
||||
OwnerData.load()
|
||||
|
||||
assert self.layout
|
||||
self.layout.use_property_split = True
|
||||
self.layout.use_property_decorate = False
|
||||
props = context.scene.BIMOwnerProperties
|
||||
props = tool.Owner.get_owner_props()
|
||||
|
||||
if not OwnerData.data["user_person"]:
|
||||
self.layout.label(text="No people found.")
|
||||
@@ -244,13 +250,14 @@ class BIM_PT_actor(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
def draw(self, context):
|
||||
if not ActorData.is_loaded:
|
||||
ActorData.load()
|
||||
|
||||
self.props = context.scene.BIMOwnerProperties
|
||||
assert self.layout
|
||||
self.props = tool.Owner.get_owner_props()
|
||||
|
||||
self.layout.use_property_split = True
|
||||
self.layout.use_property_decorate = False
|
||||
@@ -268,7 +275,8 @@ class BIM_PT_actor(bpy.types.Panel):
|
||||
for actor in ActorData.data["actors"]:
|
||||
self.draw_actor(actor)
|
||||
|
||||
def draw_actor(self, actor):
|
||||
def draw_actor(self, actor: dict[str, Any]) -> None:
|
||||
assert self.layout
|
||||
if actor["is_editing"]:
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
@@ -295,13 +303,14 @@ class BIM_PT_object_actor(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectActorData.is_loaded:
|
||||
ObjectActorData.load()
|
||||
|
||||
self.props = context.scene.BIMOwnerProperties
|
||||
assert self.layout
|
||||
self.props = tool.Owner.get_owner_props()
|
||||
|
||||
if not ObjectActorData.data["actor"]:
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -1103,8 +1103,8 @@ class LoadProjectElements(bpy.types.Operator):
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_decomposition_elements(self):
|
||||
containers = set()
|
||||
def get_decomposition_elements(self) -> set[ifcopenshell.entity_instance]:
|
||||
containers: set[ifcopenshell.entity_instance] = set()
|
||||
for filter_category in self.props.filter_categories:
|
||||
if not filter_category.is_selected:
|
||||
continue
|
||||
@@ -1116,15 +1116,15 @@ class LoadProjectElements(bpy.types.Operator):
|
||||
container = None
|
||||
elif self.file.schema != "IFC2X3" and container.is_a("IfcContext"):
|
||||
container = None
|
||||
elements = set()
|
||||
elements: set[ifcopenshell.entity_instance] = set()
|
||||
for container in containers:
|
||||
for rel in container.ContainsElements:
|
||||
elements.update(rel.RelatedElements)
|
||||
self.append_decomposed_elements(elements)
|
||||
return elements
|
||||
|
||||
def append_decomposed_elements(self, elements):
|
||||
decomposed_elements = set()
|
||||
def append_decomposed_elements(self, elements: set[ifcopenshell.entity_instance]) -> None:
|
||||
decomposed_elements: set[ifcopenshell.entity_instance] = set()
|
||||
for element in elements:
|
||||
if element.IsDecomposedBy:
|
||||
for subelement in element.IsDecomposedBy[0].RelatedObjects:
|
||||
@@ -1133,26 +1133,26 @@ class LoadProjectElements(bpy.types.Operator):
|
||||
self.append_decomposed_elements(decomposed_elements)
|
||||
elements.update(decomposed_elements)
|
||||
|
||||
def get_ifc_class_elements(self):
|
||||
elements = set()
|
||||
def get_ifc_class_elements(self) -> set[ifcopenshell.entity_instance]:
|
||||
elements: set[ifcopenshell.entity_instance] = set()
|
||||
for filter_category in self.props.filter_categories:
|
||||
if not filter_category.is_selected:
|
||||
continue
|
||||
elements.update(self.file.by_type(filter_category.name, include_subtypes=False))
|
||||
return elements
|
||||
|
||||
def get_ifc_type_elements(self):
|
||||
elements = set()
|
||||
def get_ifc_type_elements(self) -> set[ifcopenshell.entity_instance]:
|
||||
elements: set[ifcopenshell.entity_instance] = set()
|
||||
for filter_category in self.props.filter_categories:
|
||||
if not filter_category.is_selected:
|
||||
continue
|
||||
elements.update(ifcopenshell.util.element.get_types(self.file.by_id(filter_category.ifc_definition_id)))
|
||||
return elements
|
||||
|
||||
def get_whitelist_elements(self):
|
||||
def get_whitelist_elements(self) -> set[ifcopenshell.entity_instance]:
|
||||
return set(ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query))
|
||||
|
||||
def get_blacklist_elements(self):
|
||||
def get_blacklist_elements(self) -> set[ifcopenshell.entity_instance]:
|
||||
return set(self.file.by_type("IfcElement")) - set(
|
||||
ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query)
|
||||
)
|
||||
@@ -1192,6 +1192,9 @@ class LinkIfc(bpy.types.Operator, ImportHelper):
|
||||
filepath: str
|
||||
files: list[bpy.types.OperatorFileListElement]
|
||||
directory: str
|
||||
filter_glob: str
|
||||
use_relative_path: bool
|
||||
use_cache: bool
|
||||
|
||||
def draw(self, context):
|
||||
pprops = tool.Project.get_project_props()
|
||||
@@ -1432,23 +1435,11 @@ class ReloadLink(bpy.types.Operator):
|
||||
filepath: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
filepath = Path(self.filepath)
|
||||
|
||||
def get_linked_ifcs() -> set[bpy.types.Library]:
|
||||
return {
|
||||
c.library
|
||||
for c in bpy.data.collections
|
||||
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == filepath
|
||||
}
|
||||
|
||||
for library in get_linked_ifcs():
|
||||
library.reload()
|
||||
|
||||
is_abs = os.path.isabs(Path(self.filepath))
|
||||
use_relative_path = not is_abs
|
||||
bpy.ops.bim.unlink_ifc(filepath=self.filepath)
|
||||
status = bpy.ops.bim.link_ifc(filepath=self.filepath, use_cache=False, use_relative_path=use_relative_path)
|
||||
|
||||
filepath = tool.Ifc.resolve_uri(self.filepath)
|
||||
status = bpy.ops.bim.link_ifc(filepath=filepath, use_cache=False, use_relative_path=use_relative_path)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -2642,6 +2633,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
|
||||
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
|
||||
) or single_mode:
|
||||
context.workspace.status_text_set(text=None)
|
||||
self.tool_state.plane_method = None
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Polyline.move_polyline_to_measure(context, self.input_ui)
|
||||
tool.Polyline.clear_polyline()
|
||||
|
||||
@@ -35,7 +35,7 @@ from bpy.props import (
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal, Union, get_args
|
||||
from typing import TYPE_CHECKING, Literal, Union, get_args, Generator
|
||||
from typing_extensions import assert_never
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ class FilterCategory(PropertyGroup):
|
||||
class Link(PropertyGroup):
|
||||
name: StringProperty(
|
||||
name="Name",
|
||||
description="Filepath to linked .ifc file, stored in posix format (could be relative to .blend file, not to .ifc)",
|
||||
description="Filepath to linked .ifc file, stored in posix format (could be relative to .ifc file, not to .blend)",
|
||||
)
|
||||
is_loaded: BoolProperty(name="Is Loaded", default=False)
|
||||
is_selectable: BoolProperty(name="Is Selectable", default=True)
|
||||
@@ -372,7 +372,7 @@ class BIMProjectProperties(PropertyGroup):
|
||||
items=get_template_file,
|
||||
name="Template File",
|
||||
description=(
|
||||
"Template to use for a new project. All types from the template will be appended to a new project)."
|
||||
"Template to use for a new project. All types from the template will be appended to a new project."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -500,6 +500,12 @@ class BIMProjectProperties(PropertyGroup):
|
||||
return self.library_breadcrumb[-1]
|
||||
return None
|
||||
|
||||
def get_loaded_links(self) -> Generator[Link, None, None]:
|
||||
for link in self.links:
|
||||
if not link.is_loaded:
|
||||
continue
|
||||
yield link
|
||||
|
||||
|
||||
class MeasureToolSettings(PropertyGroup):
|
||||
measurement_type_items = [
|
||||
|
||||
@@ -58,7 +58,7 @@ def updateResourceName(self, context):
|
||||
attributes={"Name": self.name},
|
||||
)
|
||||
if props.active_resource_id == self.ifc_definition_id:
|
||||
attribute = props.resource_attributes.get("Name")
|
||||
attribute = props.resource_attributes["Name"]
|
||||
attribute.string_value = self.name
|
||||
bonsai.bim.module.resource.data.refresh()
|
||||
tool.Sequence.refresh_task_resources()
|
||||
|
||||
@@ -118,29 +118,31 @@ class IfcClassData:
|
||||
def representation_template(cls):
|
||||
rprops = tool.Root.get_root_props()
|
||||
ifc_class = rprops.ifc_class
|
||||
if ifc_class.startswith("IfcStructuralPoint"):
|
||||
return [("VERTEX", "Vertex", "A single 3D point")]
|
||||
elif ifc_class.startswith("IfcStructuralCurve"):
|
||||
return [("EDGE", "Edge", "A straight edge between two points")]
|
||||
elif ifc_class.startswith("IfcStructuralSurface"):
|
||||
return [("FACE", "Face", "A planar face surface")]
|
||||
templates = [
|
||||
("EMPTY", "No Geometry", "Start with an empty object"),
|
||||
None,
|
||||
(
|
||||
"OBJ",
|
||||
"Tessellation From Object",
|
||||
"Use an object as a template to create a new tessellation",
|
||||
),
|
||||
(
|
||||
"MESH",
|
||||
"Custom Tessellation",
|
||||
"Create a basic tessellated or faceted cube",
|
||||
),
|
||||
(
|
||||
"EXTRUSION",
|
||||
"Custom Extruded Solid",
|
||||
"An extrusion from an arbitrary profile",
|
||||
),
|
||||
]
|
||||
templates.extend(
|
||||
[
|
||||
(
|
||||
"OBJ",
|
||||
"Tessellation From Object",
|
||||
"Use an object as a template to create a new tessellation",
|
||||
),
|
||||
(
|
||||
"MESH",
|
||||
"Custom Tessellation",
|
||||
"Create a basic tessellated or faceted cube",
|
||||
),
|
||||
(
|
||||
"EXTRUSION",
|
||||
"Custom Extruded Solid",
|
||||
"An extrusion from an arbitrary profile",
|
||||
),
|
||||
]
|
||||
)
|
||||
if ifc_class.endswith("Type") or ifc_class.endswith("Style"):
|
||||
templates.extend(
|
||||
[
|
||||
|
||||
@@ -195,6 +195,17 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
should_add_representation: bool
|
||||
ifc_representation_class: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC project loaded.")
|
||||
return False
|
||||
# object.select_all operator's requirement and it's generally more safe.
|
||||
elif context.mode != "OBJECT":
|
||||
cls.poll_message_set(f"Can only assign class in OBJECT mode, not in {context.mode} mode.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Root.get_root_props()
|
||||
@@ -263,8 +274,15 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# Apply geometry.
|
||||
if obj.modifiers:
|
||||
ensure_single_user_mesh(obj.data)
|
||||
# In older Blender versions 'object.convert'
|
||||
# still requires object to be both selected and active.
|
||||
is_old_blender = bpy.app.version < (4, 4, 0)
|
||||
with context.temp_override(selected_editable_objects=[obj]):
|
||||
if is_old_blender:
|
||||
tool.Blender.set_active_object(obj)
|
||||
bpy.ops.object.convert(target="MESH")
|
||||
if is_old_blender:
|
||||
tool.Blender.clear_objects_selection()
|
||||
|
||||
# Apply scale.
|
||||
if obj.scale != (1, 1, 1):
|
||||
@@ -456,8 +474,15 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.representation_template = "EXTRUSION"
|
||||
props.representation_obj = None
|
||||
elif (obj := tool.Blender.get_active_object(is_selected=True)) and obj.type == "MESH":
|
||||
props.representation_template = "OBJ"
|
||||
props.representation_obj = obj
|
||||
if (
|
||||
props.ifc_class.startswith("IfcStructuralPoint")
|
||||
or props.ifc_class.startswith("IfcStructuralCurve")
|
||||
or props.ifc_class.startswith("IfcStructuralSurface")
|
||||
):
|
||||
pass # Implement auto association?
|
||||
else:
|
||||
props.representation_template = "OBJ"
|
||||
props.representation_obj = obj
|
||||
# For convenience, preselect IFC class
|
||||
if self.ifc_product:
|
||||
props.ifc_product = self.ifc_product
|
||||
@@ -666,6 +691,49 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif representation_template == "ROOF":
|
||||
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||
bpy.ops.bim.add_roof()
|
||||
elif representation_template == "VERTEX":
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
representation = builder.get_representation(ifc_context, [builder.vertex()])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
elif representation_template == "EDGE":
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
end = Vector((1, 0, 0)) / unit_scale
|
||||
representation = builder.get_representation(ifc_context, [builder.edge(end=end)])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
elif representation_template == "FACE":
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
points = [Vector(p) / unit_scale for p in ((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0))]
|
||||
representation = builder.get_representation(ifc_context, [builder.face(points)])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
bpy.context.view_layer.update() # Ensures obj.matrix_world is correct
|
||||
|
||||
|
||||
@@ -239,6 +239,7 @@ class LoadSearch(Operator, tool.Ifc.Operator):
|
||||
class ColourByProperty(Operator):
|
||||
bl_idname = "bim.colour_by_property"
|
||||
bl_label = "Colour by Property"
|
||||
bl_description = "Color all visible objects by the provided property."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -275,6 +276,8 @@ class ColourByProperty(Operator):
|
||||
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
|
||||
|
||||
for obj in context.visible_objects:
|
||||
if obj.type not in ("MESH", "CURVE"):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
@@ -365,6 +368,7 @@ class ColourByProperty(Operator):
|
||||
class SelectByProperty(Operator):
|
||||
bl_idname = "bim.select_by_property"
|
||||
bl_label = "Select by Property"
|
||||
bl_description = "Select objects based on currently selected colored property value."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -202,7 +202,12 @@ class BIMSearchProperties(PropertyGroup):
|
||||
filter_classes_index: IntProperty(name="Filter Classes Index")
|
||||
filter_container: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level")
|
||||
filter_container_index: IntProperty(name="Filter Level Index")
|
||||
show_flat_colours: BoolProperty(name="Flat Colours", default=False, update=update_show_flat_colours)
|
||||
show_flat_colours: BoolProperty(
|
||||
name="Flat Colours",
|
||||
description="Toggle flat shading in the active viewport.",
|
||||
default=False,
|
||||
update=update_show_flat_colours,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
element_key: str
|
||||
|
||||
@@ -512,7 +512,7 @@ class AssignProcess(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Assign Process"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
task: bpy.props.IntProperty()
|
||||
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
|
||||
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(tool.Sequence.RELATED_OBJECT_TYPE)],
|
||||
)
|
||||
related_object: bpy.props.IntProperty()
|
||||
@@ -549,7 +549,7 @@ class UnassignProcess(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Unassign Process"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
task: bpy.props.IntProperty()
|
||||
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
|
||||
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(tool.Sequence.RELATED_OBJECT_TYPE)],
|
||||
)
|
||||
related_object: bpy.props.IntProperty()
|
||||
|
||||
@@ -130,6 +130,7 @@ class LoadsDecorator:
|
||||
|
||||
coord = Vector(coord)
|
||||
rv3d = context.region_data
|
||||
assert rv3d and context.region
|
||||
perspective = rv3d.view_perspective
|
||||
view_matrix = rv3d.view_matrix
|
||||
point_view_space = view_matrix @ coord
|
||||
|
||||
@@ -740,6 +740,7 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
|
||||
if self.ifc_class == "IfcSurfaceStyleLighting":
|
||||
|
||||
def callback(attribute_name, _, data):
|
||||
assert attributes
|
||||
color = attributes.add()
|
||||
color.name = attribute_name
|
||||
color_value = data[attribute_name]
|
||||
@@ -815,10 +816,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"style.edit_surface_style",
|
||||
tool.Ifc.get(),
|
||||
style=self.surface_style,
|
||||
attributes={
|
||||
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
|
||||
"Transparency": self.props.transparency or None,
|
||||
},
|
||||
attributes=self.get_shading_attributes(),
|
||||
)
|
||||
tool.Loader.create_surface_style_shading(material, self.surface_style)
|
||||
elif self.surface_style.is_a() == "IfcSurfaceStyleRendering":
|
||||
@@ -912,10 +910,11 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
def get_shading_attributes(self) -> dict[str, Any]:
|
||||
return {
|
||||
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
|
||||
"Transparency": self.props.transparency or None,
|
||||
}
|
||||
attributes = {}
|
||||
attributes["SurfaceColour"] = self.color_to_dict(self.props.surface_colour)
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
attributes["Transparency"] = self.props.transparency or None
|
||||
return attributes
|
||||
|
||||
def get_rendering_attributes(self) -> dict[str, Any]:
|
||||
if self.props.is_diffuse_colour_null:
|
||||
|
||||
@@ -55,6 +55,7 @@ class BIM_PT_styles(Panel):
|
||||
row.operator("bim.load_styles", text="", icon="IMPORT").style_type = style_type
|
||||
return
|
||||
|
||||
self.is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3"
|
||||
active_style = self.props.active_style
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
|
||||
@@ -158,8 +159,9 @@ class BIM_PT_styles(Panel):
|
||||
def draw_surface_style_shading(self):
|
||||
row = self.layout.row()
|
||||
row.prop(self.props, "surface_colour")
|
||||
row = self.layout.row()
|
||||
row.prop(self.props, "transparency")
|
||||
if not self.is_ifc2x3:
|
||||
row = self.layout.row()
|
||||
row.prop(self.props, "transparency")
|
||||
|
||||
def draw_surface_style_rendering(self):
|
||||
row = self.layout.row()
|
||||
|
||||
@@ -426,7 +426,7 @@ class EnableEditingZone(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingZone(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class DisableEditingZone(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_zone"
|
||||
bl_label = "Disable Editing Zone"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@@ -70,6 +70,7 @@ class ExecuteIfcTester(bpy.types.Operator):
|
||||
|
||||
def execute_tester(self, ifc_data: ifcopenshell.file, ifc_path: str, specs_path: str) -> Union[set[str], None]:
|
||||
props = bpy.context.scene.IfcTesterProperties
|
||||
props.failed_entities.clear()
|
||||
|
||||
# No need for if-statement, just postponing lots of diffs.
|
||||
if True:
|
||||
|
||||
@@ -96,7 +96,11 @@ class BIM_PT_tester(Panel):
|
||||
op2.spec_index = props.active_specification_index
|
||||
op2.req_index = i
|
||||
|
||||
if props.old_index == props.active_specification_index and props.n_entities > 0:
|
||||
if (
|
||||
props.old_index == props.active_specification_index
|
||||
and props.n_entities > 0
|
||||
and len(props.failed_entities) > 0
|
||||
):
|
||||
row = self.layout.row()
|
||||
row.label(text=f"Failed entities [{props.n_entities}]:")
|
||||
self.layout.template_list(
|
||||
|
||||
@@ -67,7 +67,8 @@ class TypeData:
|
||||
if not relating_type_classes:
|
||||
return []
|
||||
results = []
|
||||
relating_type_class = bpy.context.active_object.BIMTypeProperties.relating_type_class
|
||||
assert (obj := bpy.context.active_object)
|
||||
relating_type_class = tool.Type.get_object_type_props(obj).relating_type_class
|
||||
if not relating_type_class and relating_type_classes:
|
||||
relating_type_class = relating_type_classes[0][0]
|
||||
elements = tool.Ifc.get().by_type(relating_type_class)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user