Merge v0.8.0 into light/newUI

This commit is contained in:
Chirag Singh
2026-04-03 01:11:16 +05:30
446 changed files with 46833 additions and 3888 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
python ../nix/cache_dependencies.py unpack python ../nix/cache_dependencies.py unpack
- name: ccache - name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20 uses: hendrikmuhs/ccache-action@v1.2.22
with: with:
key: mac-${{ matrix.arch }} key: mac-${{ matrix.arch }}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
python ../IfcOpenShell/nix/cache_dependencies.py unpack python ../IfcOpenShell/nix/cache_dependencies.py unpack
- name: ccache - name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20 uses: hendrikmuhs/ccache-action@v1.2.22
with: with:
key: ubuntu-22.04-${{ runner.arch }} key: ubuntu-22.04-${{ runner.arch }}
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
python3 ../nix/cache_dependencies.py unpack python3 ../nix/cache_dependencies.py unpack
- name: ccache - name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20 uses: hendrikmuhs/ccache-action@v1.2.22
with: with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9 key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
python3 ../nix/cache_dependencies.py unpack python3 ../nix/cache_dependencies.py unpack
- name: ccache - name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20 uses: hendrikmuhs/ccache-action@v1.2.22
with: with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9 key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
+28 -10
View File
@@ -5,11 +5,26 @@ on:
jobs: jobs:
build_ifcopenshell: build_ifcopenshell:
runs-on: windows-2022
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: ['x64'] include:
- arch: x64
runs_on: windows-2022
deps_dir: _deps-vs2022-x64-installed
vcvars: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"'
build_branch: windows-x64
zip_suffix: win64
- arch: ARM64
runs_on: windows-11-arm
deps_dir: _deps-vs2022-ARM64-installed
vcvars: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsarm64.bat"'
build_branch: windows-arm64
zip_suffix: win-arm64
runs-on: ${{ matrix.runs_on }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -20,8 +35,8 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
repository: IfcOpenShell/build-outputs repository: IfcOpenShell/build-outputs
path: _deps-vs2022-x64-installed path: ${{ matrix.deps_dir }}
ref: windows-${{ matrix.arch }} ref: ${{ matrix.build_branch }}
lfs: true lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }} token: ${{ secrets.BUILD_REPO_TOKEN }}
@@ -31,13 +46,13 @@ jobs:
- name: Unpack Dependencies - name: Unpack Dependencies
run: | run: |
cd _deps-vs2022-x64-installed cd ${{ matrix.deps_dir }}
Get-ChildItem -Path . -Filter 'cache-*.zip' | ForEach-Object { Get-ChildItem -Path . -Filter 'cache-*.zip' | ForEach-Object {
7z x $_.FullName 7z x $_.FullName
} }
- name: ccache - name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20 uses: hendrikmuhs/ccache-action@v1.2.22
with: with:
key: win-${{ matrix.arch }} key: win-${{ matrix.arch }}
# Windows ccache needs ~1GB # Windows ccache needs ~1GB
@@ -46,14 +61,16 @@ jobs:
- name: Run Build Script And Pack .zip Archives - name: Run Build Script And Pack .zip Archives
shell: cmd shell: cmd
env:
TARGET_ARCH: ${{ matrix.arch }} # lets the Python script know which arch to target (optional override)
run: | run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" call ${{ matrix.vcvars }}
cd win cd win
python build-all-win.py python build-all-win.py
- name: Pack Dependencies - name: Pack Dependencies
run: | run: |
cd _deps-vs2022-x64-installed cd ${{ matrix.deps_dir }}
Get-ChildItem -Path . -Directory | ForEach-Object { Get-ChildItem -Path . -Directory | ForEach-Object {
$cacheFile = "cache-$($_.Name).zip" $cacheFile = "cache-$($_.Name).zip"
echo $cacheFile echo $cacheFile
@@ -64,12 +81,13 @@ jobs:
- name: Commit and Push Changes to Build Repository - name: Commit and Push Changes to Build Repository
run: | run: |
cd _deps-vs2022-x64-installed cd ${{ matrix.deps_dir }}
git config user.name "IfcOpenBot" git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org" git config user.email "ifcopenbot@ifcopenshell.org"
git checkout -B ${{ matrix.build_branch }}
git add *.zip git add *.zip
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit" git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || echo "Push failed" git push --set-upstream origin ${{ matrix.build_branch }} || echo "Push failed"
- name: Configure AWS Credentials - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v6
+9 -4
View File
@@ -7,6 +7,9 @@ on:
jobs: jobs:
lint-formatting: lint-formatting:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
MIN_IOS_PY_VERSION: "3.10"
MIN_BLENDER_PY_VERSION: "3.11"
steps: steps:
- name: Action - checkout repository - name: Action - checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -14,12 +17,12 @@ jobs:
- name: Action - install python - name: Action - install python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.10" python-version: ${{ env.MIN_IOS_PY_VERSION }}
- name: Action - install python - name: Action - install python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.11" python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
- name: Install dependencies - name: Install dependencies
run: | run: |
@@ -33,8 +36,10 @@ jobs:
id: syntax-errors id: syntax-errors
run: | run: |
ERROR=0 ERROR=0
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1 # Using 2 Python versions - one minimum required for IfcOpenShell
python3.11 -W error -m compileall -q src/bonsai || ERROR=1 # and other that's used by Blender currently.
python${{ env.MIN_IOS_PY_VERSION }} -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python${{ env.MIN_BLENDER_PY_VERSION }} -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR exit $ERROR
continue-on-error: true continue-on-error: true
+1 -1
View File
@@ -109,7 +109,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo. # Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender. # Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.0/blender-5.0.1-linux-x64.tar.xz wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.0/blender-5.1.0-linux-x64.tar.xz
tar -xf blender.tar.xz tar -xf blender.tar.xz
# Setup Blender. # Setup Blender.
+35
View File
@@ -0,0 +1,35 @@
name: ci-ifcedit-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcedit &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcedit/dist
+36
View File
@@ -0,0 +1,36 @@
name: ci-ifcmcp-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcmcp &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcmcp/dist
verbose: true
@@ -24,7 +24,7 @@ jobs:
if: | if: |
github.repository == 'IfcOpenShell/IfcOpenShell' github.repository == 'IfcOpenShell/IfcOpenShell'
steps: steps:
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba - uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
with: with:
environment-name: test-env environment-name: test-env
create-args: >- create-args: >-
@@ -84,7 +84,7 @@ jobs:
run: | run: |
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/ 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@v2 # https://github.com/mamba-org/setup-micromamba - uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
with: with:
environment-name: test-env environment-name: test-env
create-args: >- create-args: >-
+6 -6
View File
@@ -35,7 +35,7 @@ jobs:
- -
name: ccache name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20 uses: hendrikmuhs/ccache-action@v1.2.22
- -
name: Build ifcopenshell name: Build ifcopenshell
@@ -91,26 +91,26 @@ jobs:
lfs: true lfs: true
- name: Download - name: Download
uses: actions/download-artifact@v8.0.0 uses: actions/download-artifact@v8.0.1
with: with:
# Artifact name # Artifact name
name: ifcos-artifacts name: ifcos-artifacts
path: artifacts/ path: artifacts/
- -
name: Set up QEMU name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- -
name: Login to Dockerhub name: Login to Dockerhub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: aecgeeks username: aecgeeks
password: ${{ secrets.DOCKER_HUB_TOKEN }} password: ${{ secrets.DOCKER_HUB_TOKEN }}
- -
name: Build container image name: Build container image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: artifacts context: artifacts
repository: aecgeeks/ifcopenshell repository: aecgeeks/ifcopenshell
@@ -24,7 +24,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
pyver: [py39, py310, py311, py312, py313, py314] pyver: [py310, py311, py312, py313, py314]
config: config:
- { - {
name: "Windows 64bit", name: "Windows 64bit",
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
pyver: [py39, py310, py311, py312, py313, py314] pyver: [py310, py311, py312, py313, py314]
config: config:
- { - {
name: "Windows 64bit", name: "Windows 64bit",
+35
View File
@@ -0,0 +1,35 @@
name: ci-ifcquery-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcquery &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcquery/dist
+3 -1
View File
@@ -79,7 +79,7 @@ jobs:
libhdf5-dev libcgal-dev libeigen3-dev libhdf5-dev libcgal-dev libeigen3-dev
- name: ccache - name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20 uses: hendrikmuhs/ccache-action@v1.2.22
with: with:
key: ubuntu-22.04-${{ runner.arch }} key: ubuntu-22.04-${{ runner.arch }}
@@ -181,6 +181,7 @@ jobs:
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \ "-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \ -DGLTF_SUPPORT=On \
-DWITH_ROCKSDB=On \ -DWITH_ROCKSDB=On \
-DBUILD_EXAMPLES=ON \
../cmake ../cmake
sudo make -j $(nproc) sudo make -j $(nproc)
sudo make install sudo make install
@@ -253,6 +254,7 @@ jobs:
cd ../ifcpatch && make test || ERROR=1 cd ../ifcpatch && make test || ERROR=1
pip install -e ../ifctester --no-deps pip install -e ../ifctester --no-deps
cd ../ifctester && make test || ERROR=1 cd ../ifctester && make test || ERROR=1
make build-ids-docs || ERROR=1
# Run mathutils related tests at the end to ensure no other code is relying on mathutils. # Run mathutils related tests at the end to ensure no other code is relying on mathutils.
cd ../ifcopenshell-python cd ../ifcopenshell-python
pip install mathutils pip install mathutils
+65
View File
@@ -0,0 +1,65 @@
name: Deploy AI chat App to static page repo
permissions:
id-token: write
pages: write
on:
push:
paths:
- 'src/ifcchat/**'
- '.github/workflows/publish-aichat-app.yaml'
branches:
- v0.8.0
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Checkout intermediate Pages repo
uses: actions/checkout@v4
with:
repository: IfcOpenShell/aichat_ifcopenshell_org_static_html
ref: gh-pages
path: output
token: ${{ secrets.WEBSITE_PUBLISH }}
- name: Sync demo app into target subfolder
run: |
rsync -av --delete --exclude='.git/' src/ifcchat/ output/
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Download wheels
working-directory: output/
run: |
pip download ifcquery==0.8.5 ifcopenshell-mcp==0.8.5 ifcedit==0.8.5 lark==1.3.1 isodate==0.7.2 --no-deps -d ./dist
- name: Commit and push if changed
working-directory: output
run: |
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
git add .
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "$(git log --oneline -1)"
git push origin gh-pages
+24 -17
View File
@@ -1,4 +1,4 @@
name: Deploy Pyodide Demo App to GitHub Pages name: Deploy Pyodide Demo App to static page repo
permissions: permissions:
id-token: write id-token: write
@@ -11,6 +11,7 @@ on:
- '.github/workflows/publish-pyodide-demo-app.yml' - '.github/workflows/publish-pyodide-demo-app.yml'
branches: branches:
- v0.8.0 - v0.8.0
workflow_dispatch:
jobs: jobs:
activate: activate:
@@ -30,21 +31,27 @@ jobs:
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
- name: Setup Pages - name: Checkout intermediate Pages repo
uses: actions/configure-pages@v5 uses: actions/checkout@v4
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v4
with: with:
path: src/pyodide/demo-app/ repository: IfcOpenShell/wasm_ifcopenshell_org_static_html
ref: gh-pages
path: output
token: ${{ secrets.WEBSITE_PUBLISH }}
- name: Sync demo app into target subfolder
run: |
rsync -av --delete --exclude='.git/' src/pyodide/demo-app/ output/
- name: Commit and push if changed
working-directory: output
run: |
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
deploy: git add .
environment: if git diff --cached --quiet; then
name: github-pages echo "No changes to commit"
url: ${{ steps.deployment.outputs.page_url }} exit 0
runs-on: ubuntu-latest fi
needs: build
steps: git commit -m "$(git log --oneline -1)"
- name: Deploy to GitHub Pages git push origin gh-pages
id: deployment
uses: actions/deploy-pages@v4
+16 -3
View File
@@ -5,6 +5,8 @@
/_installed-vs*-x*/ /_installed-vs*-x*/
/build/ /build/
/src/examples/build/ /src/examples/build/
# ifctester docs output
/src/ifctester/test/build/
# output directories # output directories
/cmake/out/ /cmake/out/
@@ -12,6 +14,7 @@
/src/ifcmax/out/ /src/ifcmax/out/
/src/ifcwrap/out/ /src/ifcwrap/out/
/src/qtviewer/out/ /src/qtviewer/out/
/src/ifctester/webapp/public/pyodide/
/win/BuildDepsCache*.txt /win/BuildDepsCache*.txt
@@ -80,10 +83,14 @@ src/ifcopenshell-python/test/build
# bonsai i18n # bonsai i18n
src/bonsai/bonsai/translations.py src/bonsai/bonsai/translations.py
# bonsai test temp files # bonsai external dependencies (cloned for just ty checks)
src/bonsai/external_dependencies/
# bonsai test temp/cache files
src/bonsai/test/files/temp src/bonsai/test/files/temp
src/bonsai/test/files/basic.ifc.cache.blend src/bonsai/test/files/*.cache.blend
src/bonsai/test/files/basic.ifc.cache.sqlite src/bonsai/test/files/*.cache.json
src/bonsai/test/files/*.cache.sqlite
# bonsai data # bonsai data
src/bonsai/bonsai/bim/data/build/ src/bonsai/bonsai/bim/data/build/
@@ -115,3 +122,9 @@ dev_environment.bat
src/ifcopenshell-python/ifcopenshell/express/*.exp src/ifcopenshell-python/ifcopenshell/express/*.exp
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
# temp files from AI coding tools
*.claude
*.py.tmp*
*.json.tmp*
+3
View File
@@ -50,10 +50,13 @@ Contents
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true) | [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) | | [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) |
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) | | [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| [ifcedit](https://docs.ifcopenshell.org/ifcedit.html) | CLI wrapper for ifcopenshell.api IFC model mutation functions | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcedit?label=PyPI&color=006dad)](https://pypi.org/project/ifcedit/) |
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) | | [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html) | [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| [ifcmcp](https://docs.ifcopenshell.org/ifcmcp.html) | MCP server for querying and editing IFC building models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcmcp?label=PyPI&color=006dad)](https://pypi.org/project/ifcmcp/) |
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) | | [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) | | [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| [ifcquery](https://docs.ifcopenshell.org/ifcquery.html) | CLI tool for querying and inspecting IFC building models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcquery?label=PyPI&color=006dad)](https://pypi.org/project/ifcquery/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true) | [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) | | [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
+19 -10
View File
@@ -13,6 +13,7 @@ import hashlib
import os import os
import pathlib import pathlib
import re import re
import subprocess
from typing import NoReturn from typing import NoReturn
from urllib import request from urllib import request
@@ -20,7 +21,7 @@ from github import Github
def get_repo_tag_names() -> list[str]: def get_repo_tag_names() -> list[str]:
git_return = os.popen("git tag -l").read() git_return = subprocess.check_output("git tag -l", text=True)
tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name] tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name]
print(f"{len(tag_names)} tag_names found in repo") print(f"{len(tag_names)} tag_names found in repo")
return tag_names return tag_names
@@ -78,6 +79,10 @@ def get_release_zip(tag: str) -> tuple[str, str]:
raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.") raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.")
def run(command: str) -> None:
subprocess.check_output(command)
start = datetime.datetime.now() start = datetime.datetime.now()
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender" URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
@@ -97,7 +102,7 @@ should_release = False
target_release_tag = "" target_release_tag = ""
TARGET_OS = "windows-x64" TARGET_OS = "windows-x64"
git_status = os.popen("git status").read() git_status = subprocess.check_output("git status", text=True)
print(git_status) print(git_status)
for tag_name in get_repo_tag_names(): for tag_name in get_repo_tag_names():
@@ -147,7 +152,7 @@ blenderbim_build_version = target_release_tag.replace("blenderbim-", "")
# url_blenderbim_py3x_win_zip # url_blenderbim_py3x_win_zip
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag) release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
os.popen(f"wget {url_blenderbim_py3x_win_zip} --no-verbose").read() subprocess.check_call(f"wget {url_blenderbim_py3x_win_zip} --no-verbose")
# sha256sum_blenderbim_py310_win_zip # sha256sum_blenderbim_py310_win_zip
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name) sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
@@ -201,13 +206,13 @@ print("[INFO] inserting dynamic chocolatey package parameters successful")
print("\n_____ build choco.exe with mono") print("\n_____ build choco.exe with mono")
choco_version = "1.1.0" choco_version = "1.1.0"
os.popen(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet").read() run(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet")
os.popen(f"tar -xzf {choco_version}.tar.gz").read() run(f"tar -xzf {choco_version}.tar.gz")
print("choco tar unpack successful") print("choco tar unpack successful")
os.chdir("choco-1.1.0") os.chdir("choco-1.1.0")
os.popen("./build.sh").read() run("./build.sh")
os.popen("cp -r build_output/chocolatey /opt/chocolatey").read() run("cp -r build_output/chocolatey /opt/chocolatey")
os.chdir(BLENDERBIM_DIR) os.chdir(BLENDERBIM_DIR)
if pathlib.Path("/opt/chocolatey/choco.exe").exists(): if pathlib.Path("/opt/chocolatey/choco.exe").exists():
@@ -215,11 +220,15 @@ if pathlib.Path("/opt/chocolatey/choco.exe").exists():
print("\n_____ build choco pack") print("\n_____ build choco pack")
os.popen("mono /opt/chocolatey/choco.exe pack --allow-unofficial").read() run("mono /opt/chocolatey/choco.exe pack --allow-unofficial")
os.popen('mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial').read() run(
'mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial'
)
print("\n_____ build choco push") print("\n_____ build choco push")
os.popen('mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose').read() run(
'mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose'
)
print(f"choco push of version: {target_release_tag} successful!") print(f"choco push of version: {target_release_tag} successful!")
print(f"it took: {datetime.datetime.now() - start}") print(f"it took: {datetime.datetime.now() - start}")
+1 -1
View File
@@ -80,7 +80,7 @@ option(BUILD_IFCGEOM "Build IfcGeom." ON)
option(BUILD_IFCPYTHON "Build IfcPython." ON) option(BUILD_IFCPYTHON "Build IfcPython." ON)
option(BUILD_CONVERT "Build IfcConvert executable." ON) option(BUILD_CONVERT "Build IfcConvert executable." ON)
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF) option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
option(BUILD_EXAMPLES "Build example applications." ON) option(BUILD_EXAMPLES "Build example applications." OFF)
option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." ON) option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." ON)
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF) option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6 option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
+3
View File
@@ -41,6 +41,9 @@ def pack_dependencies(install_dir: Path) -> None:
if not dependency_path.is_dir(): if not dependency_path.is_dir():
continue continue
dependency_name = dependency_path.name dependency_name = dependency_path.name
# Skip ifcopenshell - it's a build output, not a dependency to reuse across builds.
if dependency_name == "ifcopenshell":
continue
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz" tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
if tar_path.exists(): if tar_path.exists():
print(f"Skipping existing cache: '{tar_path}'") print(f"Skipping existing cache: '{tar_path}'")
+183 -3
View File
@@ -2,10 +2,10 @@
name = "IfcOpenShell" name = "IfcOpenShell"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"black==26.1.0", "black==26.3.1",
"ruff==0.15.4", "ruff==0.15.8",
"poethepoet", "poethepoet",
"gersemi==0.26.0", "gersemi==0.26.1",
] ]
[tool.black] [tool.black]
@@ -28,6 +28,12 @@ extend-exclude = '''
reportInvalidTypeForm = false reportInvalidTypeForm = false
disableBytesTypePromotions = true disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true reportUnnecessaryTypeIgnoreComment = true
# Pylance doesn't respect gitignore, so we have to exclude files manually here
# to avoid VS Code slowing down.
# https://github.com/microsoft/pylance-release/issues/5169
exclude = [
"_deps",
]
# Define here general ruff settings, # Define here general ruff settings,
# then they will be inherited by projects' .toml files. # then they will be inherited by projects' .toml files.
@@ -72,6 +78,139 @@ ignore = [
"UP032", # Replace .format with f-string "UP032", # Replace .format with f-string
] ]
[tool.ty.rules]
all = "ignore"
# Structural rules (no deep type inference needed, easier to adapt).
abstract-method-in-final-class = "error"
ambiguous-protocol-member = "error"
byte-string-type-annotation = "error"
conflicting-declarations = "error"
conflicting-metaclass = "error"
cyclic-class-definition = "error"
cyclic-type-alias-definition = "error"
dataclass-field-order = "error"
duplicate-base = "error"
duplicate-kw-only = "error"
empty-body = "error"
escape-character-in-forward-annotation = "error"
final-on-non-method = "error"
final-without-value = "error"
fstring-type-annotation = "error"
ignore-comment-unknown-rule = "error"
implicit-concatenated-string-type-annotation = "error"
inconsistent-mro = "error"
ineffective-final = "error"
instance-layout-conflict = "error"
invalid-dataclass = "error"
invalid-dataclass-override = "error"
invalid-enum-member-annotation = "error"
invalid-explicit-override = "error"
invalid-frozen-dataclass-subclass = "error"
invalid-generic-class = "error"
invalid-generic-enum = "error"
invalid-ignore-comment = "error"
invalid-legacy-positional-parameter = "error"
invalid-legacy-type-variable = "error"
invalid-named-tuple = "error"
invalid-newtype = "error"
invalid-overload = "error"
invalid-paramspec = "error"
invalid-protocol = "error"
invalid-syntax-in-forward-annotation = "error"
invalid-total-ordering = "error"
invalid-type-alias-type = "error"
invalid-type-checking-constant = "error"
invalid-type-guard-definition = "error"
invalid-type-variable-bound = "error"
invalid-type-variable-constraints = "error"
invalid-typed-dict-header = "error"
invalid-typed-dict-statement = "error"
override-of-final-method = "error"
override-of-final-variable = "error"
possibly-missing-import = "error"
possibly-missing-submodule = "error"
# Has false positives due to ty walrus operator bug.
# possibly-unresolved-reference = "error"
raw-string-type-annotation = "error"
redundant-final-classvar = "error"
shadowed-type-variable = "error"
subclass-of-final-class = "error"
super-call-in-named-tuple-method = "error"
unavailable-implicit-super-arguments = "error"
unbound-type-variable = "error"
undefined-reveal = "error"
unresolved-global = "error"
unresolved-import = "error"
unresolved-reference = "error"
unused-ignore-comment = "error"
unused-type-ignore-comment = "error"
useless-overload-body = "error"
# Non-structural rules:
deprecated = "error"
zero-stepsize-in-slice = "error"
possibly-missing-implicit-call = "error"
unused-awaitable = "error"
# Function argument rules:
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
# call-non-callable = "error"
conflicting-argument-forms = "error"
# Too many false positives.
# invalid-argument-type = "error"
missing-argument = "error"
parameter-already-assigned = "error"
positional-only-parameter-as-kwarg = "error"
too-many-positional-arguments = "error"
unknown-argument = "error"
# Has a lot of warnings due to current ty walrus operator issues.
# index-out-of-bounds = "error"
# unresolved-attribute = "error"
[tool.ty.environment]
extra-paths = [
"src/bonsai/external_dependencies",
"src/bcf",
"src/bsdd",
"src/bonsai",
"src/ifc4d",
"src/ifc5d",
"src/ifccityjson",
"src/ifcclash",
"src/ifccsv",
"src/ifcdiff",
"src/ifcfm",
"src/ifcopenshell-python",
"src/ifcpatch",
"src/ifctester",
]
[tool.ty.src]
exclude = [
# External dependencies cloned for type checking only.
"src/bonsai/external_dependencies",
# Submodules.
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
"src/svgfill/3rdparty",
# Has special dependencies.
"src/ifcopenshell-python/ifcopenshell/geom/app.py",
"src/ifcopenshell-python/ifcopenshell/geom/code_editor_pane.py",
"src/ifcopenshell-python/ifcopenshell/util/doc.py",
"src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py",
"src/ifcopenshell-python/ifcopenshell/util/ifc4x3dev_scrape_data_for_docs.py",
# Too esoteric.
"src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py",
"src/ifc2ca/templates",
# Too dev.
"src/bcf/setup.py",
"src/bsdd/yml_to_classes.py",
# Deprecated.
"src/ifc2ca/_deprecated",
]
[tool.poe.tasks] [tool.poe.tasks]
ruff-main = "ruff check --extend-exclude nix/build-all.py" ruff-main = "ruff check --extend-exclude nix/build-all.py"
@@ -81,6 +220,47 @@ ruff.sequence = ["ruff-main", "ruff-old"]
black = "black ." black = "black ."
ty.sequence = ["ty-bonsai", "ty-ios"]
ty.help = "Run ty type checker. Requires ty-venv to be set up first."
ty-bonsai = "ty check src/bonsai --python=src/bonsai/.venv"
ty-venv.sequence = ["ty-venv-bonsai", "ty-venv-ios"]
ty-venv-bonsai.sequence = [
{cmd = "uv venv src/bonsai/.venv --python=3.11 --allow-existing"},
{cmd = "uv pip install -r src/bonsai/type-check-requirements.txt --python=src/bonsai/.venv"},
]
ty-venv-ios.sequence = [
{cmd = "uv venv src/ifcopenshell-python/.venv --python=3.10 --allow-existing"},
{cmd = "uv pip install -r src/ifcopenshell-python/type-check-requirements.txt --python=src/ifcopenshell-python/.venv"},
]
format.sequence = ["black", "ruff-main", "ruff-old"] format.sequence = ["black", "ruff-main", "ruff-old"]
cmake-format = "gersemi . --in-place" cmake-format = "gersemi . --in-place"
[tool.poe.tasks.ty-ios]
# --ignore unresolved-reference: walrus operator false positives in ty.
cmd = """
ty check
src/bcf
src/bsdd
src/ifc2ca
src/ifc4d
src/ifc5d
src/ifccityjson
src/ifcclash
src/ifccsv
src/ifcdiff
src/ifcfm
src/ifcopenshell-python
src/ifcpatch
src/ifctester
--python=src/ifcopenshell-python/.venv
--ignore unresolved-reference
"""
[tool.poe.tasks.bonsai-deps]
help = "Clone or update Bonsai external dependencies."
cmd = "python src/bonsai/scripts/bonsai_deps.py"
+3 -3
View File
@@ -34,8 +34,8 @@ client_id, client_secret = "", ""
class OAuthReceiver(http.server.BaseHTTPRequestHandler): class OAuthReceiver(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None: def do_GET(self) -> None:
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
self.server.auth_code = query.get("code", [""])[0] # type: ignore self.server.auth_code = query.get("code", [""])[0]
self.server.auth_state = query.get("state", [""])[0] # type: ignore self.server.auth_state = query.get("state", [""])[0]
self.send_response(200) self.send_response(200)
self.send_header("Content-type", "text/plain") self.send_header("Content-type", "text/plain")
self.end_headers() self.end_headers()
@@ -255,7 +255,7 @@ class BcfClient:
project_id: str = "", project_id: str = "",
topics: str = "", topics: str = "",
query_string: Optional[str] = None, query_string: Optional[str] = None,
) -> list[Any]: ) -> None:
# return self.get( # return self.get(
# f"/projects/{project_id}/topics", # f"/projects/{project_id}/topics",
# { # {
+14 -10
View File
@@ -173,16 +173,17 @@ def assert_viewpoints(viewpoints):
assert viewpoint.snapshot is not None assert viewpoint.snapshot is not None
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None: def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo( expected_vp = mdl.VisualizationInfo(
components=mdl.Components( components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
selection=expected_selection, selection=expected_selection,
visibility=mdl.ComponentVisibility( visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
exceptions=expected_exception, exceptions=expected_exception,
default_visibility=False, default_visibility=False,
), ),
@@ -193,6 +194,7 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266), camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048), camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
field_of_view=60, field_of_view=60,
aspect_ratio=1.0,
), ),
guid="21dd4807-e9af-439e-a980-04d913a6b1ce", guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
) )
@@ -200,16 +202,17 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
assert viewpoint.snapshot is not None assert viewpoint.snapshot is not None
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None: def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo( expected_vp = mdl.VisualizationInfo(
components=mdl.Components( components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
selection=expected_selection, selection=expected_selection,
visibility=mdl.ComponentVisibility( visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
exceptions=expected_exception, exceptions=expected_exception,
default_visibility=True, default_visibility=True,
), ),
@@ -220,6 +223,7 @@ def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, ex
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428), camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241), camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
field_of_view=60, field_of_view=60,
aspect_ratio=1.0,
), ),
guid="81daa431-bf01-4a49-80a2-1ab07c177717", guid="81daa431-bf01-4a49-80a2-1ab07c177717",
) )
+8 -26
View File
@@ -48,6 +48,7 @@ VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d') VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD) LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI) LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
PYPI_IMP:=cp PYPI_IMP:=cp
ifdef PYVERSION ifdef PYVERSION
@@ -63,6 +64,7 @@ PYNUMBER:=3$(PYMINOR)
PYPI_VERSION:=3.$(PYMINOR) PYPI_VERSION:=3.$(PYMINOR)
endif # def PYVERSION endif # def PYVERSION
IFCMERGE_VERSION:=2026-04-02
ifdef PLATFORM ifdef PLATFORM
SUPPORTED_PLATFORMS := linux macos macosm1 win SUPPORTED_PLATFORMS := linux macos macosm1 win
@@ -223,19 +225,8 @@ endif
cd build/bonsai/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css cd build/bonsai/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
# Provides IFCJSON functionality # Provides IFCJSON functionality
cd build && wget -O ifc2json.zip https://github.com/IFCJSON-Team/IFC2JSON_python/archive/refs/heads/master.zip # TODO: Use official repo, once https://github.com/IFCJSON-Team/IFC2JSON_python/pull/8 is merged.
cd build && unzip ifc2json.zip && rm ifc2json.zip cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/IFC2JSON_python.git@pyproject_toml" --no-deps -w wheels/
# IFCJSON doesn't have pyproject.toml, so we use python command.
cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-*/file_converters && \
$(PYTHON) -c "from setuptools import setup; \
setup( \
name='ifcjson', \
version='0.0.1', \
author='Jan Brouwer', \
author_email='jan@brewsky.nl', \
packages=['ifcjson'], \
)" bdist_wheel
cp -r build/IFC2JSON_python-*/file_converters/dist/*.whl build/wheels/
# Brickschema requires pkg_resources which is provided by Blender. # Brickschema requires pkg_resources which is provided by Blender.
# Provides Brickschema functionality # Provides Brickschema functionality
@@ -243,26 +234,16 @@ endif
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
# Required for hipped roof generation # Required for hipped roof generation
cd build && wget https://github.com/prochitecture/bpypolyskel/archive/refs/heads/master.zip cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/prochitecture/bpypolyskel" --no-deps -w wheels/
cd build && unzip master.zip && rm master.zip
cd build && . env/$(VENV_ACTIVATE) && cd bpypolyskel-master && \
$(PYTHON) -c "from setuptools import setup; \
setup( \
name='bpypolyskel', \
version='0.0.0', \
packages=['bpypolyskel'], \
)" bdist_wheel
cp -r build/bpypolyskel-master/dist/*.whl build/wheels/
# folder for executable files # folder for executable files
mkdir -p build/bonsai/libs/bin mkdir -p build/bonsai/libs/bin
# required for three-way git merging # required for three-way git merging
ifeq ($(PLATFORM), win) ifeq ($(PLATFORM), win)
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2025-01-26/ifcmerge.zip cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/$(IFCMERGE_VERSION)/ifcmerge.exe
cd build/bonsai/libs/bin && unzip ifcmerge.zip && rm ifcmerge.zip
else else
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/$(IFCMERGE_VERSION)/ifcmerge && chmod +x ifcmerge
endif endif
# Generate translations module for Bonsai build # Generate translations module for Bonsai build
@@ -281,6 +262,7 @@ else
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml $(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py $(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py $(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml $(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif endif
+51 -36
View File
@@ -35,15 +35,15 @@ IN_PACKAGE = __package__ == "bonsai"
import platform import platform
import re import re
import traceback import traceback
import uuid
import webbrowser import webbrowser
from collections import deque from collections import deque
from collections.abc import Generator from collections.abc import Generator
from pathlib import Path from pathlib import Path
from typing import Any, Union from typing import TYPE_CHECKING, Any, Union
last_commit_hash = "8888888" last_commit_hash = "8888888"
last_commit_date = "9999999" last_commit_date = "9999999"
last_git_branch = "7777777"
def get_last_commit_hash() -> Union[str, None]: def get_last_commit_hash() -> Union[str, None]:
@@ -61,6 +61,15 @@ def get_last_commit_date() -> Union[str, None]:
return last_commit_date return last_commit_date
def get_git_branch() -> Union[str, None]:
# Using this weird way to write 7777777,
# so makefile won't accidentally replace it here
# we'll be able to distinguish branch from placeholder value.
if last_git_branch == str(7_777777):
return None
return last_git_branch
# Accessed from bonsai extension: # Accessed from bonsai extension:
bbim_semver: dict[str, Any] = {} bbim_semver: dict[str, Any] = {}
@@ -72,6 +81,21 @@ REINSTALLED_BBIM_VERSION: Union[str, None] = None
REGISTERED_BBIM_PACKAGE: str REGISTERED_BBIM_PACKAGE: str
def is_registering() -> bool:
"""
During addon registration ``bpy.context`` and ``bpy.data`` are restricted
and you can't access their properties.
"""
import bpy
if TYPE_CHECKING or bpy.app.version >= (5, 0, 0):
import _bpy_restrict_state as bpy_restrict_state
else:
import bpy_restrict_state
return isinstance(bpy.context, bpy_restrict_state._RestrictContext)
def initialize_bbim_semver(): def initialize_bbim_semver():
"""Initialize `bbim_semver` dictionary. """Initialize `bbim_semver` dictionary.
@@ -93,9 +117,13 @@ def initialize_bbim_semver():
bbim_semver["version"] = version_str bbim_semver["version"] = version_str
def get_debug_info(): def get_debug_info(*, bonsai_failed_to_load: bool = False) -> dict[str, Any]:
import bpy
bbim_version = bbim_semver["version"] bbim_version = bbim_semver["version"]
# All data here should be gettable even in case of `bpy.context` and `bpy.data` being inaccessible
# and Bonsai completely failed to load.
debug_info = { debug_info = {
"os": platform.system(), "os": platform.system(),
"os_version": platform.version(), "os_version": platform.version(),
@@ -107,10 +135,19 @@ def get_debug_info():
"bonsai_version": bbim_version, "bonsai_version": bbim_version,
"bonsai_commit_hash": get_last_commit_hash(), "bonsai_commit_hash": get_last_commit_hash(),
"bonsai_commit_date": get_last_commit_date(), "bonsai_commit_date": get_last_commit_date(),
"bonsai_git_branch": get_git_branch(),
"last_actions": last_actions, "last_actions": last_actions,
"last_error": last_error, "last_error": last_error,
} }
# Can't access blend data or context during registration.
# If Bonsai failed to load we cannot safely access any of its properties or its tools
# as they may not be registered yet and acessing them will break Bonsai Fatal Error UI.
if is_registering() or bonsai_failed_to_load:
return debug_info
import bonsai.tool as tool
# Add .blend file save information # Add .blend file save information
if bpy.data.is_saved: if bpy.data.is_saved:
debug_info["blend_file_path"] = bpy.data.filepath debug_info["blend_file_path"] = bpy.data.filepath
@@ -131,7 +168,7 @@ def get_debug_info():
return debug_info return debug_info
def format_debug_info(info: dict): def format_debug_info(info: dict[str, Any]) -> str:
last_actions = "" last_actions = ""
for action in info["last_actions"]: for action in info["last_actions"]:
last_actions += f"\n# {action['type']}: {action['name']}" last_actions += f"\n# {action['type']}: {action['name']}"
@@ -149,33 +186,10 @@ def get_binaries(path: Path) -> Generator[Path, None, None]:
yield from path.glob("**/*.so") yield from path.glob("**/*.so")
def safe_link_dlls() -> None: # TODO: remove before 0.8.6 release.
# Blender 4.2+ has a problem on Windows for disabling/enabling/reinstalling extensions # On Windows issues with removing extensions were resolved in Blender 4.3,
# with loaded binary dependencies (on Windows you can't remove a binary if it's loaded by some program). # but we removed our workaround that was producing some junk only in 0.8.5 release.
# To avoid this issue we temporary hard link dlls to our temp directory on unregister() # So we're temporarily keeping the part that's cleaning up outputs from previous releases.
# (unregister is executed before Blender will try to uninstall dependencies and the issue will arise).
# Then, Blender won't have a problem unlinking unloaded dlls as they are still linked somewhere.
# On register() we clean up our temp directory with binaries.
#
# TODO: If user uninstalls Bonsai to never use it again, temporary directory won't be cleared.
#
# See: https://projects.blender.org/blender/blender/issues/125049
import bpy
ext_path = Path(bpy.utils.user_resource("EXTENSIONS"))
local_path = ext_path / ".local"
# We use random hash subfolder as user may try to enable/disable addon multiple times.
random_hash = uuid.uuid4().hex[:8]
temp_local = ext_path / ".local_temp" / random_hash
temp_local.mkdir(parents=True)
for filepath in get_binaries(local_path):
dest_path = temp_local / filepath.relative_to(local_path)
dest_path.parent.mkdir(exist_ok=True, parents=True)
os.link(filepath, dest_path)
def clean_up_dlls_safe_links() -> None: def clean_up_dlls_safe_links() -> None:
import bpy import bpy
@@ -205,6 +219,8 @@ def clean_up_dlls_safe_links() -> None:
if IN_BLENDER: if IN_BLENDER:
import bpy
initialize_bbim_semver() initialize_bbim_semver()
def get_binary_info() -> dict[str, Any]: def get_binary_info() -> dict[str, Any]:
@@ -246,10 +262,12 @@ if IN_BLENDER:
global last_commit_hash global last_commit_hash
global last_commit_date global last_commit_date
global last_git_branch
path = Path(__file__).resolve().parent path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True) repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat() last_commit_date = repo.head.object.committed_datetime.isoformat()
last_git_branch = repo.active_branch.name
except: except:
pass pass
@@ -296,9 +314,6 @@ if IN_BLENDER:
purge_cache() purge_cache()
def unregister(): def unregister():
if platform.system() == "Windows":
safe_link_dlls()
import bonsai.bim import bonsai.bim
bonsai.bim.unregister() bonsai.bim.unregister()
@@ -333,7 +348,7 @@ if IN_BLENDER:
bl_context = "scene" bl_context = "scene"
def draw(self, context): def draw(self, context):
info = get_debug_info() info = get_debug_info(bonsai_failed_to_load=True)
layout = self.layout layout = self.layout
layout.alert = True layout.alert = True
@@ -409,7 +424,7 @@ if IN_BLENDER:
bl_description = "Copies debugging information to your clipboard for use in bugreports" bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context): def execute(self, context):
info = get_debug_info() info = get_debug_info(bonsai_failed_to_load=True)
info.update(get_binary_info()) info.update(get_binary_info())
info = format_debug_info(info) info = format_debug_info(info)
context.window_manager.clipboard = info context.window_manager.clipboard = info
+1 -3
View File
@@ -72,9 +72,7 @@ class IfcExporter:
def set_header(self): def set_header(self):
self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file) self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
self.file.header.file_name.time_stamp = ( self.file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
datetime.datetime.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
)
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version) self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.header.file_name.originating_system = "{} {}".format( self.file.header.file_name.originating_system = "{} {}".format(
self.get_application_name(), tool.Blender.get_bonsai_version() self.get_application_name(), tool.Blender.get_bonsai_version()
+8 -5
View File
@@ -45,16 +45,19 @@ from bonsai.bim.module.nest.decorator import NestDecorator
cwd = os.path.dirname(os.path.realpath(__file__)) cwd = os.path.dirname(os.path.realpath(__file__))
global_subscription_owner = object() global_subscription_owner = object()
# Separate owner for per-object msgbus subscriptions (name, active_material_index).
# Using a dedicated owner allows clearing all per-object subscriptions at once
# during undo/redo without affecting other global subscriptions.
object_subscription_owner = object()
def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None: def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None:
try: try:
obj.name obj.name
except: except:
# The object is invalid but somehow still has a callback. Clear all # The object is invalid but somehow still has a callback.
# msgbus subscriptions to prevent useless further triggers. # This can occur during undo/redo when the Python wrapper is stale.
bpy.msgbus.clear_by_owner(obj) return
return # In case the object RNA is gone during an undo / redo operation
# Blender names are up to 63 UTF-8 bytes # Blender names are up to 63 UTF-8 bytes
if len(bytes(obj.name, "utf-8")) >= 63: if len(bytes(obj.name, "utf-8")) >= 63:
return return
@@ -189,7 +192,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
return return
bpy.msgbus.subscribe_rna( bpy.msgbus.subscribe_rna(
key=subscribe_to, key=subscribe_to,
owner=obj, owner=object_subscription_owner,
args=( args=(
obj, obj,
data_path, data_path,
+4 -9
View File
@@ -316,11 +316,8 @@ class IfcStore:
del IfcStore.id_map[data["id"]] del IfcStore.id_map[data["id"]]
if "guid" in data: if "guid" in data:
del IfcStore.guid_map[data["guid"]] del IfcStore.guid_map[data["guid"]]
obj = IfcStore.get_object_by_name(data["obj"]) # Note: msgbus subscriptions are cleared globally during
if obj is None: # rebuild_element_maps which runs after every undo/redo.
# obj was just created during this step and didn't existed before.
return
bpy.msgbus.clear_by_owner(obj)
@staticmethod @staticmethod
def commit_link_element(data: OperationData) -> None: def commit_link_element(data: OperationData) -> None:
@@ -367,10 +364,8 @@ class IfcStore:
del IfcStore.id_map[data["id"]] del IfcStore.id_map[data["id"]]
if "guid" in data: if "guid" in data:
del IfcStore.guid_map[data["guid"]] del IfcStore.guid_map[data["guid"]]
obj = IfcStore.get_object_by_name(data["obj"]) # Note: msgbus subscriptions are cleared globally during
# obj might be removed after unlink. # rebuild_element_maps which runs after every undo/redo.
if not obj:
bpy.msgbus.clear_by_owner(obj)
@staticmethod @staticmethod
def unlink_element( def unlink_element(
+5 -3
View File
@@ -64,8 +64,8 @@ class MaterialCreator:
mesh: Union[OBJECT_DATA_TYPE, None], mesh: Union[OBJECT_DATA_TYPE, None],
shape_has_openings: bool, shape_has_openings: bool,
) -> None: ) -> None:
if ((rep := getattr(element, "Representation", ...) is not ...) and not rep) or ( if ((rep := getattr(element, "Representation", ...)) is not ... and not rep) or (
(rep := getattr(element, "RepresentationMaps", ...) is not ...) and not rep (rep := getattr(element, "RepresentationMaps", ...)) is not ... and not rep
): ):
return return
@@ -747,6 +747,7 @@ class IfcImporter:
self.update_progress((percent_average / 100 * progress_range) + start_progress) self.update_progress((percent_average / 100 * progress_range) + start_progress)
shape = iterator.get() shape = iterator.get()
if shape: if shape:
assert isinstance(shape, W.TriangulationElement)
product = self.file.by_id(shape.id) product = self.file.by_id(shape.id)
self.create_product(product, shape) self.create_product(product, shape)
results.add(product) results.add(product)
@@ -1020,7 +1021,8 @@ class IfcImporter:
obj.hide_select = True obj.hide_select = True
obj.hide_viewport = True obj.hide_viewport = True
self.project["blender"].objects.link(obj) self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj collection_props = tool.Blender.get_collection_props(self.project["blender"])
collection_props.obj = obj
props = tool.Blender.get_object_bim_props(obj) props = tool.Blender.get_object_bim_props(obj)
props.collection = self.collections[project.GlobalId] = self.project["blender"] props.collection = self.collections[project.GlobalId] = self.project["blender"]
@@ -101,7 +101,7 @@ class AggregateDecorator:
cls.is_installed = False cls.is_installed = False
def dotted_line_shader(self): def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out.smooth("FLOAT", "v_ArcLength") vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo() shader_info = gpu.types.GPUShaderCreateInfo()
+11 -2
View File
@@ -16,8 +16,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy.props from typing import TYPE_CHECKING
import bpy.types
import bpy
class AuginProperties(bpy.types.PropertyGroup): class AuginProperties(bpy.types.PropertyGroup):
@@ -27,3 +28,11 @@ class AuginProperties(bpy.types.PropertyGroup):
project_name: bpy.props.StringProperty(name="Project Name") project_name: bpy.props.StringProperty(name="Project Name")
project_filename: bpy.props.StringProperty(name="IFC Filename") project_filename: bpy.props.StringProperty(name="IFC Filename")
is_success: bpy.props.BoolProperty(name="Is Successful Upload", default=False) is_success: bpy.props.BoolProperty(name="Is Successful Upload", default=False)
if TYPE_CHECKING:
username: str
password: str
token: str
project_name: str
project_filename: str
is_success: bool
+2 -2
View File
@@ -1253,8 +1253,8 @@ class ActivateBcfViewpoint(bpy.types.Operator):
else: else:
obj.data.show_background_images = False obj.data.show_background_images = False
area = next(area for area in context.screen.areas if area.type == "VIEW_3D") assert (space := tool.Blender.get_view3d_space())
area.spaces[0].region_3d.view_perspective = "CAMERA" space.region_3d.view_perspective = "CAMERA"
if self.file: if self.file:
self.set_viewpoint_components(viewpoint, context) self.set_viewpoint_components(viewpoint, context)
+1 -1
View File
@@ -230,7 +230,7 @@ class BcfTopic(PropertyGroup):
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global RELATED_TOPICS_ENUM_ITEMS global RELATED_TOPICS_ENUM_ITEMS # ty: ignore[unresolved-global]
props = self props = self
active_topic = props.active_topic active_topic = props.active_topic
active_related_topics = active_topic.related_topics.keys() active_related_topics = active_topic.related_topics.keys()
@@ -27,6 +27,7 @@ import ifcopenshell.api
import ifcopenshell.api.boundary import ifcopenshell.api.boundary
import ifcopenshell.api.root import ifcopenshell.api.root
import ifcopenshell.geom import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement import ifcopenshell.util.placement
import ifcopenshell.util.shape import ifcopenshell.util.shape
@@ -376,6 +377,8 @@ class EnableEditingBoundary(bpy.types.Operator):
obj = tool.Ifc.get_object(entity) obj = tool.Ifc.get_object(entity)
if entity and obj: if entity and obj:
setattr(bprops, blender_property, obj) setattr(bprops, blender_property, obj)
bprops.physical_or_virtual = boundary.PhysicalOrVirtualBoundary or "NOTDEFINED"
bprops.internal_or_external = boundary.InternalOrExternalBoundary or "NOTDEFINED"
return {"FINISHED"} return {"FINISHED"}
@@ -391,6 +394,8 @@ class DisableEditingBoundary(bpy.types.Operator):
bprops.is_editing = False bprops.is_editing = False
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items(): for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
setattr(bprops, blender_property, None) setattr(bprops, blender_property, None)
bprops.physical_or_virtual = "NOTDEFINED"
bprops.internal_or_external = "NOTDEFINED"
return {"FINISHED"} return {"FINISHED"}
@@ -410,6 +415,8 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
obj = getattr(bprops, blender_property, None) obj = getattr(bprops, blender_property, None)
entity = tool.Ifc.get_entity(obj) entity = tool.Ifc.get_entity(obj)
attributes[blender_property] = entity attributes[blender_property] = entity
attributes["physical_or_virtual"] = bprops.physical_or_virtual
attributes["internal_or_external"] = bprops.internal_or_external
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes) ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
bpy.ops.bim.disable_editing_boundary() bpy.ops.bim.disable_editing_boundary()
return {"FINISHED"} return {"FINISHED"}
@@ -701,6 +708,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
while True: while True:
tree.add_element(iterator.get_native()) tree.add_element(iterator.get_native())
shape = iterator.get() shape = iterator.get()
assert isinstance(shape, W.TriangulationElement)
shapes[shape.id] = { shapes[shape.id] = {
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry), "verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
"faces": ifcopenshell.util.shape.get_faces(shape.geometry), "faces": ifcopenshell.util.shape.get_faces(shape.geometry),
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Union
import bpy import bpy
from bpy.props import ( from bpy.props import (
BoolProperty, BoolProperty,
EnumProperty,
PointerProperty, PointerProperty,
) )
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
@@ -50,12 +51,43 @@ def element_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object
return False return False
def get_internal_or_external_items(
self: "BIMObjectBoundaryProperties", context: bpy.types.Context | None
) -> list[tuple[str, str, str]]:
items = [
("INTERNAL", "Internal", ""),
("EXTERNAL", "External", ""),
]
ifc = tool.Ifc.get()
if not ifc or ifc.schema != "IFC2X3":
items += [
("EXTERNAL_EARTH", "External Earth", ""),
("EXTERNAL_WATER", "External Water", ""),
("EXTERNAL_FIRE", "External Fire", ""),
]
items.append(("NOTDEFINED", "Not Defined", ""))
return items
class BIMObjectBoundaryProperties(PropertyGroup): class BIMObjectBoundaryProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing") is_editing: BoolProperty(name="Is Editing")
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter) relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter) related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter) parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter) corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
physical_or_virtual: EnumProperty(
name="PhysicalOrVirtualBoundary",
items=[
("PHYSICAL", "Physical", ""),
("VIRTUAL", "Virtual", ""),
("NOTDEFINED", "Not Defined", ""),
],
default="NOTDEFINED",
)
internal_or_external: EnumProperty(
name="InternalOrExternalBoundary",
items=get_internal_or_external_items,
)
if TYPE_CHECKING: if TYPE_CHECKING:
is_editing: bool is_editing: bool
@@ -63,6 +95,8 @@ class BIMObjectBoundaryProperties(PropertyGroup):
related_building_element: Union[bpy.types.Object, None] related_building_element: Union[bpy.types.Object, None]
parent_boundary: Union[bpy.types.Object, None] parent_boundary: Union[bpy.types.Object, None]
corresponding_boundary: Union[bpy.types.Object, None] corresponding_boundary: Union[bpy.types.Object, None]
physical_or_virtual: str
internal_or_external: str # values depend on schema: IFC2X3 omits EXTERNAL_EARTH/WATER/FIRE
class BIMBoundaryProperties(PropertyGroup): class BIMBoundaryProperties(PropertyGroup):
@@ -77,6 +77,10 @@ class BIM_PT_Boundary(Panel):
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element") self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary") self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary") self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
row = self.layout.row()
row.prop(self.bprops, "physical_or_virtual")
row = self.layout.row()
row.prop(self.bprops, "internal_or_external")
else: else:
row = self.layout.row() row = self.layout.row()
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit") row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
@@ -84,6 +88,8 @@ class BIM_PT_Boundary(Panel):
self.draw_relation_data(boundary, "RelatedBuildingElement") self.draw_relation_data(boundary, "RelatedBuildingElement")
self.draw_relation_data(boundary, "ParentBoundary") self.draw_relation_data(boundary, "ParentBoundary")
self.draw_relation_data(boundary, "CorrespondingBoundary") self.draw_relation_data(boundary, "CorrespondingBoundary")
self.draw_enum_data(boundary, "PhysicalOrVirtualBoundary")
self.draw_enum_data(boundary, "InternalOrExternalBoundary")
if hasattr(boundary, "InnerBoundaries"): if hasattr(boundary, "InnerBoundaries"):
for i, inner_boundary in enumerate(getattr(boundary, "InnerBoundaries", ())): for i, inner_boundary in enumerate(getattr(boundary, "InnerBoundaries", ())):
row = self.layout.row(align=True) row = self.layout.row(align=True)
@@ -110,6 +116,11 @@ class BIM_PT_Boundary(Panel):
else: else:
row.label(text="") row.label(text="")
def draw_enum_data(self, boundary, ifc_attribute: str):
row = self.layout.row(align=True)
row.label(text=ifc_attribute)
row.label(text=getattr(boundary, ifc_attribute, "") or "")
def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str): def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str):
if hasattr(boundary, ifc_attribute): if hasattr(boundary, ifc_attribute):
row = self.layout.row(align=True) row = self.layout.row(align=True)
+4 -4
View File
@@ -46,26 +46,26 @@ def get_libraries(self, context):
def get_namespaces(self, context): def get_namespaces(self, context):
global NAMESPACES_ENUM_ITEMS global NAMESPACES_ENUM_ITEMS # ty: ignore[unresolved-global]
NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces] NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
return NAMESPACES_ENUM_ITEMS return NAMESPACES_ENUM_ITEMS
def get_brick_entity_classes(self, context): def get_brick_entity_classes(self, context):
global ENTITY_CLASSES_ENUM_ITEMS global ENTITY_CLASSES_ENUM_ITEMS # ty: ignore[unresolved-global]
entity = self.brick_entity_create_type entity = self.brick_entity_create_type
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]] ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
return ENTITY_CLASSES_ENUM_ITEMS return ENTITY_CLASSES_ENUM_ITEMS
def get_brick_roots(self, context): def get_brick_roots(self, context):
global BRICK_ROOTS_ENUM_ITEMS global BRICK_ROOTS_ENUM_ITEMS # ty: ignore[unresolved-global]
BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes] BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes]
return BRICK_ROOTS_ENUM_ITEMS return BRICK_ROOTS_ENUM_ITEMS
def get_brick_relations(self, context): def get_brick_relations(self, context):
global BRICK_RELATIONS_ENUM_ITEMS global BRICK_RELATIONS_ENUM_ITEMS # ty: ignore[unresolved-global]
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships] BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
for relation in BrickschemaData.data["active_relations"]: for relation in BrickschemaData.data["active_relations"]:
if relation["predicate_name"] == "label": if relation["predicate_name"] == "label":
+12 -1
View File
@@ -16,9 +16,18 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING
import bpy
from bpy.types import Panel, UIList from bpy.types import Panel, UIList
import bonsai.tool as tool import bonsai.tool as tool
if TYPE_CHECKING:
from bonsai.bim.module.brick.prop import Brick
from bonsai.bim.helper import prop_with_search from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData from bonsai.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData
from bonsai.tool.brick import BrickStore from bonsai.tool.brick import BrickStore
@@ -274,7 +283,9 @@ class BIM_PT_brickschema_viewport(Panel):
class BIM_UL_bricks(UIList): class BIM_UL_bricks(UIList):
split_screen = False split_screen = False
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self, context, layout: bpy.types.UILayout, data, item: Brick, icon, active_data, active_propname
) -> None:
if item: if item:
split = layout.split(factor=0.85, align=True) split = layout.split(factor=0.85, align=True)
row = split.row() row = split.row()
@@ -37,6 +37,7 @@ messages = {
class CadTrimExtend(bpy.types.Operator): class CadTrimExtend(bpy.types.Operator):
bl_idname = "bim.cad_trim_extend" bl_idname = "bim.cad_trim_extend"
bl_label = "CAD Trim / Extend" bl_label = "CAD Trim / Extend"
bl_description = "Extends/reduces element to 3D cursor"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -82,6 +83,7 @@ class CadTrimExtend(bpy.types.Operator):
class CadMitre(bpy.types.Operator): class CadMitre(bpy.types.Operator):
bl_idname = "bim.cad_mitre" bl_idname = "bim.cad_mitre"
bl_label = "CAD Mitre" bl_label = "CAD Mitre"
bl_description = "Joins two non-parallel paths at their intersection"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
+59 -21
View File
@@ -106,23 +106,37 @@ class CadTool(WorkSpaceTool):
) )
row = layout.row(align=True) row = layout.row(align=True)
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator( add_layout_hotkey_operator(
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
) )
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context) add_layout_hotkey_operator(
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) add_layout_hotkey_operator(
row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context) add_layout_hotkey_operator(
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context) add_layout_hotkey_operator(
row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context) add_layout_hotkey_operator(
row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context) add_layout_hotkey_operator(
row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__.split("\n", 1)[1].strip(), ui_context
)
elif ( elif (
isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES) isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
@@ -132,15 +146,21 @@ class CadTool(WorkSpaceTool):
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
) )
row = layout.row(align=True) row = layout.row(align=True)
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator( add_layout_hotkey_operator(
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
) )
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context) add_layout_hotkey_operator(
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) add_layout_hotkey_operator(
row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
)
else: else:
if ( if (
@@ -168,19 +188,37 @@ class CadTool(WorkSpaceTool):
add_layout_hotkey_operator(row, "Set Gable Roof Angle", "S_R", "Set Gable Roof Angle", ui_context) add_layout_hotkey_operator(row, "Set Gable Roof Angle", "S_R", "Set Gable Roof Angle", ui_context)
row = layout.row(align=True) row = layout.row(align=True)
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator( add_layout_hotkey_operator(
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
) )
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context) add_layout_hotkey_operator(
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) add_layout_hotkey_operator(
row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context) add_layout_hotkey_operator(
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context) add_layout_hotkey_operator(
row,
"2-Point Arc",
"S_C",
bpy.ops.bim.cad_arc_from_2_points.__doc__.split("\n", 1)[1].strip(),
ui_context,
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row,
"3-Point Arc",
"S_V",
bpy.ops.bim.cad_arc_from_3_points.__doc__.split("\n", 1)[1].strip(),
ui_context,
)
class CadHotkey(bpy.types.Operator): class CadHotkey(bpy.types.Operator):
@@ -457,9 +457,7 @@ class HideClash(bpy.types.Operator):
def execute(self, context): def execute(self, context):
ClashDecorator.uninstall() ClashDecorator.uninstall()
for area in context.screen.areas: tool.Blender.update_all_viewports(context)
if area.type == "VIEW_3D":
area.tag_redraw()
return {"FINISHED"} return {"FINISHED"}
+44 -15
View File
@@ -26,10 +26,11 @@ from bpy.types import Panel, UIList
import bonsai.bim.helper import bonsai.bim.helper
import bonsai.bim.module.cost.prop as CostProp import bonsai.bim.module.cost.prop as CostProp
import bonsai.tool as tool import bonsai.tool as tool
from bonsai.bim.module.cost.data import CostSchedulesData from bonsai.bim.module.cost.data import CostItem, CostSchedulesData
if TYPE_CHECKING: if TYPE_CHECKING:
from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity
from bonsai.bim.prop import StrProperty
class BIM_PT_cost_schedules(Panel): class BIM_PT_cost_schedules(Panel):
@@ -398,8 +399,8 @@ class BIM_PT_cost_item_types(Panel):
op = row2.operator("bim.calculate_cost_item_resource_value", text="", icon="DISC") op = row2.operator("bim.calculate_cost_item_resource_value", text="", icon="DISC")
op.cost_item = cost_item.ifc_definition_id op.cost_item = cost_item.ifc_definition_id
rtprops = context.scene.BIMResourceTreeProperties
rprops = tool.Resource.get_resource_props() rprops = tool.Resource.get_resource_props()
rtprops = rprops.tree
if rtprops.resources and rprops.active_resource_index < len(rtprops.resources): if rtprops.resources and rprops.active_resource_index < len(rtprops.resources):
if has_quantity_names: if has_quantity_names:
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES") op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
@@ -661,9 +662,18 @@ class BIM_UL_cost_items_trait:
split2.alignment = "LEFT" split2.alignment = "LEFT"
split2.label(text="Rate") split2.label(text="Rate")
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMCostProperties,
item: CostProp.CostItem,
icon,
active_data,
active_propname,
) -> None:
if item: if item:
self.props = tool.Cost.get_cost_props() self.props = data
cost_item = CostSchedulesData.data["cost_items"][item.ifc_definition_id] cost_item = CostSchedulesData.data["cost_items"][item.ifc_definition_id]
row = layout.row(align=True) row = layout.row(align=True)
@@ -694,7 +704,7 @@ class BIM_UL_cost_items_trait:
# TODO: reimplement "bim.copy_cost_item_values" somewhere with better UX # TODO: reimplement "bim.copy_cost_item_values" somewhere with better UX
def draw_parent_operator(self, row, cost_item_id): def draw_parent_operator(self, row: bpy.types.UILayout, cost_item_id: int) -> None:
if self.props.active_cost_item_id: if self.props.active_cost_item_id:
if self.props.active_cost_item_id != cost_item_id: if self.props.active_cost_item_id != cost_item_id:
op = row.operator("bim.change_parent_cost_item", text="", icon="LINKED", emboss=False).new_parent = ( op = row.operator("bim.change_parent_cost_item", text="", icon="LINKED", emboss=False).new_parent = (
@@ -703,7 +713,7 @@ class BIM_UL_cost_items_trait:
else: else:
row.label(text="", icon="BLANK1") row.label(text="", icon="BLANK1")
def draw_hierarchy(self, row, item): def draw_hierarchy(self, row: bpy.types.UILayout, item: CostProp.CostItem) -> None:
for i in range(0, item.level_index): for i in range(0, item.level_index):
row.label(text="", icon="BLANK1") row.label(text="", icon="BLANK1")
if item.has_children: if item.has_children:
@@ -749,7 +759,7 @@ class BIM_UL_cost_items_trait:
else: else:
row.label(text="-") row.label(text="-")
def draw_value_column(self, layout, cost_item): def draw_value_column(self, layout: bpy.types.UILayout, cost_item: CostItem) -> None:
if cost_item["TotalAppliedValue"]: if cost_item["TotalAppliedValue"]:
text = "{0:,.2f}".format(cost_item["TotalAppliedValue"]).replace(",", " ") text = "{0:,.2f}".format(cost_item["TotalAppliedValue"]).replace(",", " ")
if cost_item["UnitBasisValueComponent"] not in [None, 1]: if cost_item["UnitBasisValueComponent"] not in [None, 1]:
@@ -760,13 +770,13 @@ class BIM_UL_cost_items_trait:
else: else:
layout.label(text="-") layout.label(text="-")
def draw_total_cost_column(self, layout, cost_item): def draw_total_cost_column(self, layout: bpy.types.UILayout, cost_item: CostItem) -> None:
format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ") format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ")
currency = CostSchedulesData.data["currency"] currency = CostSchedulesData.data["currency"]
text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers
layout.label(text=text) layout.label(text=text)
def draw_order_operator(self, row, ifc_definition_id, cost_item): def draw_order_operator(self, row: bpy.types.UILayout, ifc_definition_id: int, cost_item: CostItem) -> None:
if cost_item["NestingIndex"] is not None: if cost_item["NestingIndex"] is not None:
if cost_item["NestingIndex"] == 0: if cost_item["NestingIndex"] == 0:
op = row.operator("bim.reorder_cost_item_nesting", icon="TRIA_DOWN", text="") op = row.operator("bim.reorder_cost_item_nesting", icon="TRIA_DOWN", text="")
@@ -793,12 +803,14 @@ class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
def draw_quantity_column(self, layout, cost_item): def draw_quantity_column(self, layout, cost_item):
self.draw_uom_column(layout, cost_item) self.draw_uom_column(layout, cost_item)
def draw_total_cost_column(self, layout, cost_item): def draw_total_cost_column(self, layout: bpy.types.UILayout, cost_item: CostItem) -> None:
pass # No such thing as a total cost in a schedule of rates pass # No such thing as a total cost in a schedule of rates
class BIM_UL_cost_columns(UIList): class BIM_UL_cost_columns(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self, context, layout: bpy.types.UILayout, data, item: StrProperty, icon, active_data, active_propname
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
row.prop(item, "name", emboss=False, text="") row.prop(item, "name", emboss=False, text="")
@@ -806,9 +818,17 @@ class BIM_UL_cost_columns(UIList):
class BIM_UL_cost_item_types(UIList): class BIM_UL_cost_item_types(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
props = tool.Cost.get_cost_props() self,
cost_item = props.cost_items[props.active_cost_item_index] context,
layout: bpy.types.UILayout,
data: BIMCostProperties,
item: CostProp.CostItemType,
icon,
active_data,
active_propname,
) -> None:
cost_item = data.cost_items[data.active_cost_item_index]
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
@@ -844,7 +864,16 @@ class BIM_UL_cost_item_quantities(UIList):
class BIM_UL_product_cost_items(UIList): class BIM_UL_product_cost_items(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self,
context,
layout: bpy.types.UILayout,
data,
item: CostItemQuantity,
icon,
active_data,
active_propname,
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
op = row.operator("bim.highlight_product_cost_item", text="", icon="STYLUS_PRESSURE") op = row.operator("bim.highlight_product_cost_item", text="", icon="STYLUS_PRESSURE")
@@ -98,8 +98,8 @@ class VisualiseDiff(bpy.types.Operator):
obj.color = (0.0, 1.0, 0.0, 1.0) obj.color = (0.0, 1.0, 0.0, 1.0)
elif global_id in diff["changed"]: elif global_id in diff["changed"]:
obj.color = (0.0, 0.0, 1.0, 1.0) obj.color = (0.0, 0.0, 1.0, 1.0)
area = next(area for area in context.screen.areas if area.type == "VIEW_3D") assert (space := tool.Blender.get_view3d_space())
area.spaces[0].shading.color_type = "OBJECT" space.shading.color_type = "OBJECT"
return {"FINISHED"} return {"FINISHED"}
@@ -19,6 +19,7 @@
import json import json
import bpy import bpy
import ifcopenshell.util.element
import bonsai.core.document as core import bonsai.core.document as core
import bonsai.tool as tool import bonsai.tool as tool
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Literal, Union
import bpy import bpy
from bpy.props import ( from bpy.props import (
@@ -66,7 +66,7 @@ class Document(PropertyGroup):
tree_depth: int tree_depth: int
has_children: bool has_children: bool
is_expanded: bool is_expanded: bool
document_type: str document_type: Literal["PROJECT", "INFORMATION", "REFERENCE"]
class DocumentObject(PropertyGroup): class DocumentObject(PropertyGroup):
+34 -6
View File
@@ -16,9 +16,22 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING
import bpy
from bpy.types import Panel, UIList from bpy.types import Panel, UIList
import bonsai.tool as tool import bonsai.tool as tool
if TYPE_CHECKING:
from bonsai.bim.module.document.prop import (
BIMDocumentProperties,
Document,
DocumentObject,
)
from bonsai.bim.helper import draw_attributes from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData
@@ -207,7 +220,16 @@ class BIM_PT_object_documents(Panel):
class BIM_UL_documents(UIList): class BIM_UL_documents(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMDocumentProperties,
item: Document,
icon,
active_data,
active_propname,
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
indent_depth = 0 indent_depth = 0
@@ -252,16 +274,22 @@ class BIM_UL_documents(UIList):
class BIM_UL_document_objects(UIList): class BIM_UL_document_objects(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMDocumentProperties,
item: DocumentObject,
icon,
active_data,
active_propname,
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
row.prop(item, "name", text="", emboss=False, icon="OBJECT_DATA") row.prop(item, "name", text="", emboss=False, icon="OBJECT_DATA")
row.operator("bim.select_object", text="", icon="RESTRICT_SELECT_OFF").obj_name = item.name row.operator("bim.select_object", text="", icon="RESTRICT_SELECT_OFF").obj_name = item.name
props = tool.Document.get_document_props() if document := data.active_document:
if props.active_document:
document = props.active_document
op = row.operator("bim.unassign_document", text="", icon="X") op = row.operator("bim.unassign_document", text="", icon="X")
op.document = document.ifc_definition_id op.document = document.ifc_definition_id
op.obj = item.name op.obj = item.name
@@ -22,7 +22,9 @@ from pathlib import Path
from typing import Any, Union from typing import Any, Union
import bpy import bpy
import ifcopenshell.util.classification
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.unit import ifcopenshell.util.unit
from natsort import natsorted from natsort import natsorted
@@ -70,9 +70,9 @@ class profile_consequential:
cls.start_time = None cls.start_time = None
lines = "\n".join(cls.lines) lines = "\n".join(cls.lines)
print(lines) print(lines)
import pyperclip
pyperclip.copy(lines) assert (wm := bpy.context.window_manager)
wm.clipboard = lines
cls.lines = [] cls.lines = []
@@ -1787,7 +1787,7 @@ class CutDecorator:
# Handle both old float64 and new float32 checksums for version compatibility # Handle both old float64 and new float32 checksums for version compatibility
rot_checksum_bytes: bytes = eval(DecoratorData.camera_rotation_checksum) rot_checksum_bytes: bytes = eval(DecoratorData.camera_rotation_checksum)
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9) rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9).reshape(3, 3)
rot_real = tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()) rot_real = tool.Blender.np_array_legacy(obj.matrix_world.to_3x3())
rot_dot = np.dot(rot_check, rot_real.T) rot_dot = np.dot(rot_check, rot_real.T)
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1)) angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
@@ -151,7 +151,7 @@ from bpy_extras.view3d_utils import (
) )
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from ifcopenshell.util.unit import si_conversions from ifcopenshell.util.unit import si_conversions
from mathutils import Matrix, Vector from mathutils import Matrix, Vector, geometry
from mathutils.geometry import intersect_line_line from mathutils.geometry import intersect_line_line
from mathutils.kdtree import KDTree from mathutils.kdtree import KDTree
@@ -1243,9 +1243,7 @@ class SnapManager:
@staticmethod @staticmethod
def _redraw_viewport() -> None: def _redraw_viewport() -> None:
"""Force 3D viewport redraw.""" """Force 3D viewport redraw."""
for area in bpy.context.screen.areas: tool.Blender.update_all_viewports()
if area.type == "VIEW_3D":
area.tag_redraw()
def build_snap_cache( def build_snap_cache(
self, context: bpy.types.Context, active_obj: bpy.types.Object, include_active: bool = False self, context: bpy.types.Context, active_obj: bpy.types.Object, include_active: bool = False
@@ -1287,7 +1285,7 @@ class SnapManager:
continue continue
coords = np.empty(vertex_count * 3, dtype=np.float32) coords = np.empty(vertex_count * 3, dtype=np.float32)
mesh.vertices.foreach_get("co", coords) # type: ignore[arg-type] mesh.vertices.foreach_get("co", coords)
coords = coords.reshape(-1, 3) coords = coords.reshape(-1, 3)
matrix = np.array(obj_eval.matrix_world, dtype=np.float32) matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
@@ -456,7 +456,8 @@ def format_distance(
tx_dist = fmt % d_cm tx_dist = fmt % d_cm
else: else:
tx_dist = fmt % value assert f"Unexpected unit_system - '{unit_system}'."
# tx_dist = fmt % value
return tx_dist return tx_dist
@@ -42,6 +42,7 @@ import bmesh
import bpy import bpy
import ifcopenshell import ifcopenshell
import ifcopenshell.api.document import ifcopenshell.api.document
import ifcopenshell.api.geometry
import ifcopenshell.api.pset import ifcopenshell.api.pset
import ifcopenshell.api.style import ifcopenshell.api.style
import ifcopenshell.geom import ifcopenshell.geom
@@ -1425,6 +1426,7 @@ class CreateDrawing(bpy.types.Operator):
"/Pset_.*Common/.Status", "/Pset_.*Common/.Status",
"EPset_Status.Status", "EPset_Status.Status",
"EPset_Status.UserDefinedStatus", "EPset_Status.UserDefinedStatus",
"Material.Name",
] ]
group = root.find("{http://www.w3.org/2000/svg}g") group = root.find("{http://www.w3.org/2000/svg}g")
@@ -3304,9 +3306,8 @@ class AddTextLiteral(bpy.types.Operator):
attr.data_type = "string" attr.data_type = "string"
attr.string_value = literal_attr_values[attr_name] attr.string_value = literal_attr_values[attr_name]
box_alignment_mask = [False] * 9 literal_props.align_vertical = "bottom"
box_alignment_mask[6] = True # bottom_left box_alignment literal_props.align_horizontal = "left"
literal_props.box_alignment = box_alignment_mask
return {"FINISHED"} return {"FINISHED"}
@@ -3364,57 +3365,55 @@ class OrderTextLiteralDown(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
# Ifc Operator is unnecessary, because suboperator is handling IFC changes. class AssignSelectedObjectAsProduct(bpy.types.Operator, tool.Ifc.Operator):
class AssignSelectedObjectAsProduct(bpy.types.Operator):
bl_idname = "bim.assign_selected_as_product" bl_idname = "bim.assign_selected_as_product"
bl_label = "Assign Selected Object As Product" bl_label = "Assign Selected Object As Product"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if len(context.selected_objects) != 2: if len(context.selected_objects) < 2:
cls.poll_message_set("2 objects need to be selected") cls.poll_message_set("At least 2 objects need to be selected")
return False return False
return True return True
def execute(self, context): def _execute(self, context):
assert bpy.context.view_layer
objs = context.selected_objects[:] objs = context.selected_objects[:]
obj1, obj2 = objs ifc_objs = [(o, tool.Ifc.get_entity(o)) for o in objs if tool.Ifc.get_entity(o)]
element1 = tool.Ifc.get_entity(obj1)
element2 = tool.Ifc.get_entity(obj2)
assert element1 and element2
# Check if at least one object is an IfcAnnotation annotations = [(o, e) for o, e in ifc_objs if e.is_a("IfcAnnotation")]
is_annotation1 = element1.is_a("IfcAnnotation") non_annotations = [(o, e) for o, e in ifc_objs if not e.is_a("IfcAnnotation")]
is_annotation2 = element2.is_a("IfcAnnotation")
if not (is_annotation1 or is_annotation2): if not annotations:
self.report({"ERROR"}, "At least one of the selected objects must be IfcAnnotation.") self.report({"ERROR"}, "At least one selected object must be an IfcAnnotation.")
return {"CANCELLED"} return {"CANCELLED"}
# If both are annotations, use the currently active object as relating product if len(non_annotations) == 1:
if is_annotation1 and is_annotation2: # One product, one or more annotations — assign all annotations to the product.
product = non_annotations[0][1]
elif len(non_annotations) == 0 and len(annotations) == 2:
# Both objects are annotations — use the non-active one as the relating product.
active_obj = context.active_object active_obj = context.active_object
if active_obj == obj1: if annotations[0][0] == active_obj:
other_selected_object = obj1 annotation_obj, annotation = annotations[0]
bpy.context.view_layer.objects.active = obj2 product = annotations[1][1]
else: else:
other_selected_object = obj2 annotation_obj, annotation = annotations[1]
bpy.context.view_layer.objects.active = obj1 product = annotations[0][1]
# If only one is an annotation, make it the active object core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product)
elif is_annotation1: tool.Blender.update_viewport()
other_selected_object = obj2 return
bpy.context.view_layer.objects.active = obj1
else: else:
other_selected_object = obj1 self.report(
bpy.context.view_layer.objects.active = obj2 {"ERROR"},
"Select exactly one product object and one or more IfcAnnotation objects.",
)
return {"CANCELLED"}
assert (active_obj := context.active_object) for annotation_obj, _ in annotations:
props = tool.Drawing.get_object_assigned_product_props(active_obj) core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product)
props.relating_product = other_selected_object
bpy.ops.bim.edit_assigned_product() tool.Blender.update_viewport()
return {"FINISHED"}
class EditAssignedProduct(bpy.types.Operator, tool.Ifc.Operator): class EditAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
@@ -3886,8 +3885,7 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)) image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
params = {"check_existing": False} image = load_image(abs_path.name, str(abs_path.parent), check_existing=False)
image = load_image(abs_path.name, str(abs_path.parent), **params)
mesh = bpy.data.meshes.new(image_filepath.stem) mesh = bpy.data.meshes.new(image_filepath.stem)
obj = bpy.data.objects.new(image_filepath.stem, mesh) obj = bpy.data.objects.new(image_filepath.stem, mesh)
@@ -4177,10 +4175,7 @@ class SelectSimilarTextLiteralValue(bpy.types.Operator):
should_select = True should_select = True
break break
elif self.attribute_type == "box_alignment": elif self.attribute_type == "box_alignment":
box_alignment_attr = next( if literal.get_box_alignment() == self.literal_value:
(attr for attr in literal.attributes if attr.name == "BoxAlignment"), None
)
if box_alignment_attr and box_alignment_attr.string_value == self.literal_value:
should_select = True should_select = True
break break
+35 -43
View File
@@ -27,7 +27,6 @@ import ifcopenshell.api.pset
import ifcopenshell.util.element import ifcopenshell.util.element
from bpy.props import ( from bpy.props import (
BoolProperty, BoolProperty,
BoolVectorProperty,
CollectionProperty, CollectionProperty,
EnumProperty, EnumProperty,
FloatProperty, FloatProperty,
@@ -673,20 +672,6 @@ class BIMCameraProperties(PropertyGroup):
return ortho_scale, aspect_ratio return ortho_scale, aspect_ratio
DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
BOX_ALIGNMENT_POSITIONS = [
"top-left",
"top-middle",
"top-right",
"middle-left",
"center",
"middle-right",
"bottom-left",
"bottom-middle",
"bottom-right",
]
class ElementValueRow(PropertyGroup): class ElementValueRow(PropertyGroup):
"""Represents a single element value row with category, key, and formatted value""" """Represents a single element value row with category, key, and formatted value"""
@@ -789,40 +774,38 @@ def get_category_items_with_counts(self, context):
class LiteralProps(PropertyGroup): class LiteralProps(PropertyGroup):
def set_box_alignment(self, new_value):
markers = new_value.count(True)
if not markers:
return
if markers > 1:
prev_value = self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
# looking for the first value changed to positive
first_changed_value = next((i for i in range(9) if new_value[i] and new_value[i] != prev_value[i]), None)
# if nothing have changed we just keep the previous value
if first_changed_value is None:
return
new_value = [False] * 9
new_value[first_changed_value] = True
self["box_alignment"] = new_value
position_string = BOX_ALIGNMENT_POSITIONS[next(i for i in range(9) if new_value[i])]
self.attributes["BoxAlignment"].set_value(position_string)
def get_box_alignment(self):
return self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
attributes: CollectionProperty(name="Attributes", type=Attribute) attributes: CollectionProperty(name="Attributes", type=Attribute)
box_alignment: BoolVectorProperty(
name="Box alignment", size=9, set=set_box_alignment, get=get_box_alignment, default=DEFAULT_BOX_ALIGNMENT
)
ifc_definition_id: IntProperty(name="IFC definition ID", default=0) ifc_definition_id: IntProperty(name="IFC definition ID", default=0)
align_horizontal: EnumProperty(
items=[
("left", "Left", "", "ALIGN_LEFT", 0),
("middle", "Middle", "", "ALIGN_CENTER", 1),
("right", "Right", "", "ALIGN_RIGHT", 2),
],
default="left",
name="Horizontal Alignment",
)
align_vertical: EnumProperty(
items=[
("top", "Top", "", "ALIGN_TOP", 0),
("middle", "Middle", "", "ALIGN_MIDDLE", 1),
("bottom", "Bottom", "", "ALIGN_BOTTOM", 2),
],
default="middle",
name="Vertical Alignment",
)
def get_box_alignment(self) -> str:
alignment = self.align_vertical + "-" + self.align_horizontal
if alignment == "middle-middle":
alignment = "center"
return alignment
def get_literal_edited_data(self) -> dict[str, str]: def get_literal_edited_data(self) -> dict[str, str]:
text_data = { text_data = {
"CurrentValue": self.attributes["Literal"].string_value, "CurrentValue": self.attributes["Literal"].string_value,
"Literal": self.attributes["Literal"].string_value, "Literal": self.attributes["Literal"].string_value,
"BoxAlignment": self.attributes["BoxAlignment"].string_value, "BoxAlignment": self.get_box_alignment(),
} }
return text_data return text_data
@@ -860,12 +843,19 @@ class LiteralProps(PropertyGroup):
if TYPE_CHECKING: if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute] attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
value: str value: str
box_alignment: tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]
ifc_definition_id: int ifc_definition_id: int
align_horizontal: str
align_vertical: str
element_value_rows: bpy.types.bpy_prop_collection_idprop[ElementValueRow] element_value_rows: bpy.types.bpy_prop_collection_idprop[ElementValueRow]
category_for_adding: str category_for_adding: str
def update_text_alignment(self, context):
for literal_props in self.literals:
literal_props.align_horizontal = self.align_horizontal
literal_props.align_vertical = self.align_vertical
class BIMTextProperties(PropertyGroup): class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False) is_editing: BoolProperty(name="Is Editing", default=False)
literals: CollectionProperty(name="Literals", type=LiteralProps) literals: CollectionProperty(name="Literals", type=LiteralProps)
@@ -899,6 +889,7 @@ class BIMTextProperties(PropertyGroup):
], ],
default="left", default="left",
name="Horizontal Alignment", name="Horizontal Alignment",
update=update_text_alignment,
) )
align_vertical: EnumProperty( align_vertical: EnumProperty(
items=[ items=[
@@ -908,6 +899,7 @@ class BIMTextProperties(PropertyGroup):
], ],
default="middle", default="middle",
name="Vertical Alignment", name="Vertical Alignment",
update=update_text_alignment,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -366,9 +366,6 @@ class BaseLinesShader(BaseShader):
} }
""" """
def __init__(self, gap_size=16):
super().__init__(gap_size=gap_size)
def glenable(self): def glenable(self):
super().glenable() super().glenable()
+4 -28
View File
@@ -781,33 +781,10 @@ class BIM_PT_text(Panel):
if other_attributes: if other_attributes:
bonsai.bim.helper.draw_attributes(other_attributes, box) bonsai.bim.helper.draw_attributes(other_attributes, box)
row = box.row(align=True) row = box.row()
cols = [row.column(align=True) for j in range(3)] row.label(text="Alignment")
for j in range(9): row.prop(literal_props, "align_horizontal", text="", expand=True)
cols[j % 3].prop( row.prop(literal_props, "align_vertical", text="", expand=True)
literal_props,
"box_alignment",
text="",
index=j,
icon="RADIOBUT_ON" if literal_props.box_alignment[j] else "RADIOBUT_OFF",
)
col = row.column(align=True)
alignment_label_row = col.row(align=True)
alignment_label_row.label(text=" Text box alignment:")
box_alignment_value = (
literal_props.attributes[
next(
(idx for idx, attr in enumerate(literal_props.attributes) if attr.name == "BoxAlignment"),
-1,
)
].string_value
if any(attr.name == "BoxAlignment" for attr in literal_props.attributes)
else "N/A"
)
col.label(text=f" {box_alignment_value}")
def draw(self, context): def draw(self, context):
obj = context.active_object obj = context.active_object
@@ -839,7 +816,6 @@ class BIM_PT_text(Panel):
for i, literal_data in enumerate(text_data["Literals"]): for i, literal_data in enumerate(text_data["Literals"]):
box = self.layout.box() box = self.layout.box()
box.label(text=f"Literal[{i}]:")
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106 # Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
for attribute in literal_data: for attribute in literal_data:
@@ -1066,7 +1066,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
cls.poll_message_set("Only available from Outliner.") cls.poll_message_set("Only available from Outliner.")
return False return False
def execute(self, context): def execute(self, context): # ty:ignore[override-of-final-method]
if len(getattr(context, "selected_ids", [])) == 0: if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"} return {"FINISHED"}
@@ -2289,7 +2289,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
elif obj in pprops.clipping_planes_objs: elif obj in pprops.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited") self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element: elif element:
if not obj.data: if not obj.data or obj.type not in ("MESH", "CURVE"):
self.report({"INFO"}, "No geometry to edit") self.report({"INFO"}, "No geometry to edit")
elif tool.Geometry.is_locked(element): elif tool.Geometry.is_locked(element):
self.report({"ERROR"}, lock_error_message(obj.name)) self.report({"ERROR"}, lock_error_message(obj.name))
+3 -3
View File
@@ -515,6 +515,8 @@ class BIM_PT_derived_coordinates(Panel):
return context.active_object is not None return context.active_object is not None
def draw(self, context): def draw(self, context):
assert context.active_object
props = tool.Model.get_model_props()
if not DerivedCoordinatesData.is_loaded: if not DerivedCoordinatesData.is_loaded:
DerivedCoordinatesData.load() DerivedCoordinatesData.load()
@@ -529,10 +531,8 @@ class BIM_PT_derived_coordinates(Panel):
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.enabled = False row.enabled = False
area_3d = next((area for area in context.screen.areas if area.type == "VIEW_3D"), None)
space_3d = next((space for space in area_3d.spaces if space.type == "VIEW_3D"), None)
if bpy.context.scene.BIMModelProperties.show_bounding_box: if props.show_bounding_box:
for axis, icon, idx in [("X", "STRIP_COLOR_01", 0), ("Y", "STRIP_COLOR_04", 1), ("Z", "STRIP_COLOR_05", 2)]: for axis, icon, idx in [("X", "STRIP_COLOR_01", 0), ("Y", "STRIP_COLOR_04", 1), ("Z", "STRIP_COLOR_05", 2)]:
row.label(text="", icon=icon) row.label(text="", icon=icon)
row.prop(context.active_object, "dimensions", text=axis, index=idx) row.prop(context.active_object, "dimensions", text=axis, index=idx)
@@ -139,7 +139,9 @@ def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.typ
tool.Georeference.set_coordinates( tool.Georeference.set_coordinates(
"blender", "blender",
ifcopenshell.util.geolocation.enh2xyz( ifcopenshell.util.geolocation.enh2xyz(
*local_coordinates, local_coordinates[0],
local_coordinates[1],
local_coordinates[2],
float(props.blender_offset_x), float(props.blender_offset_x),
float(props.blender_offset_y), float(props.blender_offset_y),
float(props.blender_offset_z), float(props.blender_offset_z),
@@ -162,7 +164,9 @@ def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types
tool.Georeference.set_coordinates( tool.Georeference.set_coordinates(
"blender", "blender",
ifcopenshell.util.geolocation.enh2xyz( ifcopenshell.util.geolocation.enh2xyz(
*local_coordinates, local_coordinates[0],
local_coordinates[1],
local_coordinates[2],
float(props.blender_offset_x), float(props.blender_offset_x),
float(props.blender_offset_y), float(props.blender_offset_y),
float(props.blender_offset_z), float(props.blender_offset_z),
@@ -267,6 +271,8 @@ class BIMGeoreferenceProperties(PropertyGroup):
x_axis_ordinate: str x_axis_ordinate: str
x_axis_is_null: bool x_axis_is_null: bool
model_is_georeferenced: bool
model_crs: str
model_origin: str model_origin: str
model_origin_si: str model_origin_si: str
model_project_north: str model_project_north: str
+14 -1
View File
@@ -16,6 +16,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from typing import TYPE_CHECKING
import bpy
from bpy.props import BoolProperty, CollectionProperty, EnumProperty, StringProperty from bpy.props import BoolProperty, CollectionProperty, EnumProperty, StringProperty
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
@@ -24,7 +27,7 @@ from bonsai.bim.prop import StrProperty
class BIMCityJsonProperties(PropertyGroup): class BIMCityJsonProperties(PropertyGroup):
def get_lods(self, context): def get_lods(self, context):
global LODS_ENUM_ITEMS global LODS_ENUM_ITEMS # ty: ignore[unresolved-global]
LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods] LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
return LODS_ENUM_ITEMS return LODS_ENUM_ITEMS
@@ -37,3 +40,13 @@ class BIMCityJsonProperties(PropertyGroup):
lod: EnumProperty(name="LOD", description="", items=get_lods, options={"ANIMATABLE"}, default=None) lod: EnumProperty(name="LOD", description="", items=get_lods, options={"ANIMATABLE"}, default=None)
is_lod_found: BoolProperty(name="Is LOD Found", default=False) is_lod_found: BoolProperty(name="Is LOD Found", default=False)
load_after_convert: BoolProperty(name="Load After Converting", default=True) load_after_convert: BoolProperty(name="Load After Converting", default=True)
if TYPE_CHECKING:
input: str
output: str
name: str
split_lod: bool
lods: bpy.types.bpy_prop_collection_idprop[StrProperty]
lod: str
is_lod_found: bool
load_after_convert: bool
+64 -79
View File
@@ -21,65 +21,71 @@ class IfcGitData:
@classmethod @classmethod
def load(cls): def load(cls):
repo = None
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
repo = tool.IfcGit.repo_from_path(path_ifc)
cls.data = { cls.data = {
"repo": cls.repo(), "repo": repo,
"remotes": cls.remotes(), "remotes": repo.remotes if repo else None,
"branch_names": cls.branch_names(), "branch_names": cls.branch_names(repo),
"remote_names": cls.remote_names(), "tag_names": cls.tag_names(repo),
"remote_urls": cls.remote_urls(), "remote_names": cls.remote_names(repo),
"remote_urls": {r.name: r.url for r in repo.remotes} if repo else {},
"path_ifc": cls.path_ifc(), "path_ifc": cls.path_ifc(),
"branches_by_hexsha": cls.branches_by_hexsha(), "branches_by_hexsha": cls.branches_by_hexsha(),
"tags_by_hexsha": cls.tags_by_hexsha(), "tags_by_hexsha": cls.tags_by_hexsha(),
"name_ifc": cls.name_ifc(), "name_ifc": cls.name_ifc(repo),
"dir_name": cls.dir_name(), "dir_name": cls.dir_name(),
"base_name": cls.base_name(), "base_name": cls.base_name(),
"working_dir": cls.working_dir(), "working_dir": repo.working_dir if repo else None,
"untracked_files": cls.untracked_files(), "ifc_is_untracked": cls.ifc_is_untracked(repo),
"is_detached": cls.is_detached(), "is_detached": repo.head.is_detached if repo else None,
"active_branch_name": cls.active_branch_name(), "active_branch_name": repo.active_branch.name if repo and not repo.head.is_detached else None,
"is_dirty": cls.is_dirty(), "is_dirty": cls.is_dirty(repo),
"commit": cls.commit(), "current_revision": cls.current_revision(repo),
"current_revision": cls.current_revision(),
"git_exe": cls.git_exe(), "git_exe": cls.git_exe(),
"ifcmerge_exe": cls.ifcmerge_exe(), "ifcmerge_exe": cls.ifcmerge_exe(),
} }
cls.is_loaded = True cls.is_loaded = True
@classmethod @classmethod
def repo(cls): def branch_names(cls, repo):
if bool(tool.Ifc.get()): if not repo or not repo.heads:
path_ifc = tool.Ifc.get_path() return []
if os.path.isfile(path_ifc): names = sorted([b.name for b in repo.branches])
return tool.IfcGit.repo_from_path(path_ifc) if "main" in names:
return None names.remove("main")
names = ["main"] + names
if repo.remotes:
for remote in repo.remotes:
for ref in remote.refs:
names.append(ref.name)
return names
@classmethod @classmethod
def remotes(cls): def tag_names(cls, repo):
if cls.repo(): if not repo:
return cls.repo().remotes return []
return None return [t.name for t in repo.tags]
@classmethod @classmethod
def branch_names(cls): def remote_names(cls, repo):
return [] if not repo:
return []
@classmethod names = sorted([r.name for r in repo.remotes])
def remote_names(cls): if "origin" in names:
return [] names.remove("origin")
names = ["origin"] + names
@classmethod return names
def remote_urls(cls):
result = {}
if cls.repo():
for remote in cls.repo().remotes:
result[remote.name] = remote.url
return result
@classmethod @classmethod
def path_ifc(cls): def path_ifc(cls):
path_ifc = tool.Ifc.get_path() path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc): if os.path.isfile(path_ifc):
return tool.Ifc.get_path() return path_ifc
return None return None
@classmethod @classmethod
@@ -88,7 +94,8 @@ class IfcGitData:
if tool.IfcGitRepo.repo.branches: if tool.IfcGitRepo.repo.branches:
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo) return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
except AttributeError: except AttributeError:
return {} pass
return {}
@classmethod @classmethod
def tags_by_hexsha(cls): def tags_by_hexsha(cls):
@@ -97,12 +104,11 @@ class IfcGitData:
return {} return {}
@classmethod @classmethod
def name_ifc(cls): def name_ifc(cls, repo):
if bool(tool.Ifc.get()): if bool(tool.Ifc.get()) and repo:
path_ifc = tool.Ifc.get_path() path_ifc = tool.Ifc.get_path()
if tool.IfcGitRepo.repo and os.path.isfile(path_ifc): if os.path.isfile(path_ifc):
working_dir = tool.IfcGitRepo.repo.working_dir return os.path.relpath(path_ifc, repo.working_dir)
return os.path.relpath(path_ifc, working_dir)
return None return None
@classmethod @classmethod
@@ -122,49 +128,28 @@ class IfcGitData:
return None return None
@classmethod @classmethod
def working_dir(cls): def ifc_is_untracked(cls, repo):
if cls.repo(): """Return True if the IFC file exists in the repo but has not been added to git."""
return cls.repo().working_dir if not repo:
return False
path_ifc = tool.Ifc.get_path()
if not os.path.isfile(path_ifc):
return False
return not bool(repo.git.ls_files(path_ifc))
@classmethod @classmethod
def untracked_files(cls): def is_dirty(cls, repo):
if cls.repo(): if repo and cls.git_exe():
return cls.repo().untracked_files
return []
@classmethod
def is_detached(cls):
if cls.repo():
return cls.repo().head.is_detached
@classmethod
def active_branch_name(cls):
if cls.repo() and not cls.is_detached():
return cls.repo().active_branch.name
@classmethod
def is_dirty(cls):
if cls.repo() and cls.git_exe():
path_ifc = tool.Ifc.get_path() path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc): if os.path.isfile(path_ifc):
return cls.repo().is_dirty(path=path_ifc) return repo.is_dirty(path=path_ifc)
return False return False
@classmethod @classmethod
def commit(cls): def current_revision(cls, repo):
props = tool.IfcGit.get_ifcgit_props() props = tool.IfcGit.get_ifcgit_props()
if cls.repo() and len(props.ifcgit_commits) > 0: if repo and repo.head.is_valid() and len(props.ifcgit_commits) > 0:
item = props.ifcgit_commits[props.commit_index] return repo.commit()
try:
return cls.repo().commit(rev=item.hexsha)
except ValueError:
return
@classmethod
def current_revision(cls):
props = tool.IfcGit.get_ifcgit_props()
if cls.repo() and cls.repo().head.is_valid() and len(props.ifcgit_commits) > 0:
return tool.IfcGitRepo.repo.commit()
@classmethod @classmethod
def git_exe(cls): def git_exe(cls):
+46 -22
View File
@@ -120,11 +120,11 @@ class CommitChanges(bpy.types.Operator):
if props.commit_message == "": if props.commit_message == "":
return False return False
if repo: if repo:
if props.new_branch_name in [branch.name for branch in repo.branches]: if props.new_branch_name in IfcGitData.data["branch_names"]:
cls.poll_message_set("Branch already exists!") cls.poll_message_set("Branch already exists!")
return False return False
elif not tool.IfcGit.is_valid_ref_format(props.new_branch_name): elif not tool.IfcGit.is_valid_ref_format(props.new_branch_name):
if repo.head.is_detached: if IfcGitData.data["is_detached"]:
cls.poll_message_set("Branch name is invalid or empty!") cls.poll_message_set("Branch name is invalid or empty!")
return False return False
elif props.new_branch_name != "": elif props.new_branch_name != "":
@@ -134,10 +134,17 @@ class CommitChanges(bpy.types.Operator):
def execute(self, context): def execute(self, context):
repo = IfcGitData.data["repo"] props = tool.IfcGit.get_ifcgit_props()
core.commit_changes(tool.IfcGit, tool.Ifc, repo) commit_message = props.commit_message
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc) new_branch_name = props.new_branch_name
core.commit_changes(tool.IfcGit, tool.Ifc, commit_message, new_branch_name)
props.new_branch_name = ""
props.commit_message = ""
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh() refresh()
IfcGitData.load()
if new_branch_name:
props.display_branch = new_branch_name
return {"FINISHED"} return {"FINISHED"}
@@ -157,7 +164,7 @@ class AddTag(bpy.types.Operator):
repo = IfcGitData.data["repo"] repo = IfcGitData.data["repo"]
if repo and ( if repo and (
not tool.IfcGit.is_valid_ref_format(props.new_tag_name) not tool.IfcGit.is_valid_ref_format(props.new_tag_name)
or props.new_tag_name in [tag.name for tag in repo.tags] or props.new_tag_name in IfcGitData.data["tag_names"]
): ):
return False return False
return True return True
@@ -165,8 +172,12 @@ class AddTag(bpy.types.Operator):
def execute(self, context): def execute(self, context):
repo = IfcGitData.data["repo"] repo = IfcGitData.data["repo"]
core.add_tag(tool.IfcGit, repo) props = tool.IfcGit.get_ifcgit_props()
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc) item = props.ifcgit_commits[props.commit_index]
core.add_tag(tool.IfcGit, repo, item.hexsha, props.new_tag_name, props.new_tag_message)
props.new_tag_name = ""
props.new_tag_message = ""
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh() refresh()
return {"FINISHED"} return {"FINISHED"}
@@ -183,7 +194,7 @@ class DeleteTag(bpy.types.Operator):
repo = IfcGitData.data["repo"] repo = IfcGitData.data["repo"]
core.delete_tag(tool.IfcGit, repo, self.tag_name) core.delete_tag(tool.IfcGit, repo, self.tag_name)
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc) core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh() refresh()
return {"FINISHED"} return {"FINISHED"}
@@ -205,8 +216,7 @@ class RefreshGit(bpy.types.Operator):
def execute(self, context): def execute(self, context):
repo = IfcGitData.data["repo"] core.refresh_revision_list(tool.IfcGit, tool.Ifc)
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
refresh() refresh()
tool.IfcGit.decolourise() tool.IfcGit.decolourise()
return {"FINISHED"} return {"FINISHED"}
@@ -284,7 +294,7 @@ class Merge(bpy.types.Operator):
def execute(self, context): def execute(self, context):
if core.merge_branch(tool.IfcGit, tool.Ifc, self): if core.merge_branch(tool.IfcGit, tool.Ifc, self) is not False:
refresh() refresh()
return {"FINISHED"} return {"FINISHED"}
else: else:
@@ -314,9 +324,9 @@ class Fetch(bpy.types.Operator):
def execute(self, context): def execute(self, context):
props = tool.IfcGit.get_ifcgit_props() props = tool.IfcGit.get_ifcgit_props()
repo = IfcGitData.data["repo"] core.fetch(tool.IfcGit, props.select_remote)
remote = repo.remotes[props.select_remote] core.refresh_revision_list(tool.IfcGit, tool.Ifc)
remote.fetch() refresh()
return {"FINISHED"} return {"FINISHED"}
@@ -336,7 +346,7 @@ class AddRemote(bpy.types.Operator):
not repo not repo
or not tool.IfcGit.is_valid_ref_format(props.remote_name) or not tool.IfcGit.is_valid_ref_format(props.remote_name)
or not props.remote_url or not props.remote_url
or props.remote_name in [remote.name for remote in repo.remotes] or props.remote_name in IfcGitData.data["remote_names"]
): ):
return False return False
return True return True
@@ -344,8 +354,11 @@ class AddRemote(bpy.types.Operator):
def execute(self, context): def execute(self, context):
repo = IfcGitData.data["repo"] repo = IfcGitData.data["repo"]
core.add_remote(tool.IfcGit, repo) props = tool.IfcGit.get_ifcgit_props()
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc) core.add_remote(tool.IfcGit, repo, props.remote_name, props.remote_url)
props.remote_name = ""
props.remote_url = ""
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh() refresh()
return {"FINISHED"} return {"FINISHED"}
@@ -360,8 +373,19 @@ class DeleteRemote(bpy.types.Operator):
def execute(self, context): def execute(self, context):
repo = IfcGitData.data["repo"] repo = IfcGitData.data["repo"]
core.delete_remote(tool.IfcGit, repo) props = tool.IfcGit.get_ifcgit_props()
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc) remote_name = props.select_remote
if props.display_branch.startswith(remote_name + "/"):
active = IfcGitData.data["active_branch_name"]
if active:
props.display_branch = active
else:
local_branches = [b for b in IfcGitData.data["branch_names"] if "/" not in b]
if local_branches:
props.display_branch = local_branches[0]
core.delete_remote(tool.IfcGit, repo, remote_name)
tool.IfcGit.select_first_remote()
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh() refresh()
return {"FINISHED"} return {"FINISHED"}
@@ -375,8 +399,8 @@ class ObjectLog(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not (obj := context.active_object): if not (obj := context.active_object) or not obj.select_get():
cls.poll_message_set("No Active Object") cls.poll_message_set("No selected object")
elif not tool.Blender.get_ifc_definition_id(obj): elif not tool.Blender.get_ifc_definition_id(obj):
cls.poll_message_set("Active Object doesn't have an IFC definition") cls.poll_message_set("Active Object doesn't have an IFC definition")
else: else:
+7 -19
View File
@@ -17,28 +17,14 @@ from bonsai.bim.module.ifcgit.data import IfcGitData
def git_branches(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS: def git_branches(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
# NOTE "Python must keep a reference to the strings returned by # NOTE "Python must keep a reference to the strings returned by
# the callback or Blender will misbehave or even crash" # the callback or Blender will misbehave or even crash"
IfcGitData.data["branch_names"] = sorted([branch.name for branch in IfcGitData.data["repo"].heads]) # Branch list (local + remote, main first) is computed once in IfcGitData.load()
IfcGitData.make_sure_is_loaded()
if "main" in IfcGitData.data["branch_names"]: return [(name, name, name) for name in IfcGitData.data["branch_names"]]
IfcGitData.data["branch_names"].remove("main")
IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"]
if IfcGitData.data["remotes"]:
for remote in IfcGitData.data["remotes"]:
for remote_branch in remote.refs:
IfcGitData.data["branch_names"].append(remote_branch.name)
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
def git_remotes(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS: def git_remotes(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
IfcGitData.data["remote_names"] = sorted([remote.name for remote in IfcGitData.data["remotes"]]) IfcGitData.make_sure_is_loaded()
return [(name, name, name) for name in IfcGitData.data["remote_names"]]
if "origin" in IfcGitData.data["remote_names"]:
IfcGitData.data["remote_names"].remove("origin")
IfcGitData.data["remote_names"] = ["origin"] + IfcGitData.data["remote_names"]
return [(myname, myname, myname) for myname in IfcGitData.data["remote_names"]]
def update_revlist(self: "IfcGitProperties", context: bpy.types.Context) -> None: def update_revlist(self: "IfcGitProperties", context: bpy.types.Context) -> None:
@@ -90,6 +76,7 @@ class IfcGitListItem(PropertyGroup):
name="Commit Message", name="Commit Message",
default="", default="",
) )
committed_date: IntProperty(name="Committed Date", default=0)
tags: CollectionProperty(type=IfcGitTag, name="List of revision tags") tags: CollectionProperty(type=IfcGitTag, name="List of revision tags")
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -98,6 +85,7 @@ class IfcGitListItem(PropertyGroup):
author_name: str author_name: str
author_email: str author_email: str
message: str message: str
committed_date: int
tags: bpy.types.bpy_prop_collection_idprop[IfcGitTag] tags: bpy.types.bpy_prop_collection_idprop[IfcGitTag]
+6 -12
View File
@@ -52,7 +52,7 @@ class IFCGIT_PT_panel(bpy.types.Panel):
if IfcGitData.data["repo"] and os.path.exists(IfcGitData.data["repo"].git_dir): if IfcGitData.data["repo"] and os.path.exists(IfcGitData.data["repo"].git_dir):
name_ifc = IfcGitData.data["name_ifc"] name_ifc = IfcGitData.data["name_ifc"]
row.label(text=IfcGitData.data["working_dir"], icon="SYSTEM") row.label(text=IfcGitData.data["working_dir"], icon="SYSTEM")
if name_ifc in IfcGitData.data["untracked_files"]: if IfcGitData.data["ifc_is_untracked"]:
row.operator( row.operator(
"ifcgit.addfile", "ifcgit.addfile",
text="Add '" + name_ifc + "' to repository", text="Add '" + name_ifc + "' to repository",
@@ -216,13 +216,7 @@ class COMMIT_UL_List(bpy.types.UIList):
): ):
current_revision = IfcGitData.data["current_revision"] current_revision = IfcGitData.data["current_revision"]
current_hexsha = current_revision.hexsha if current_revision else None
# TODO Figure how this "item" can be acesse in "data.py"
# so it's possible to move the ".commit"
try:
commit = IfcGitData.data["repo"].commit(rev=item.hexsha)
except ValueError:
return
lookup = IfcGitData.data["branches_by_hexsha"] lookup = IfcGitData.data["branches_by_hexsha"]
refs = "" refs = ""
@@ -236,11 +230,11 @@ class COMMIT_UL_List(bpy.types.UIList):
for tag in lookup[item.hexsha]: for tag in lookup[item.hexsha]:
refs += "{" + tag.name + "} " refs += "{" + tag.name + "} "
if commit == current_revision: if item.hexsha == current_hexsha:
layout.label(text="[HEAD] " + refs + commit.message.split("\n")[0], icon="DECORATE_KEYFRAME") layout.label(text="[HEAD] " + refs + item.message.split("\n")[0], icon="DECORATE_KEYFRAME")
else: else:
layout.label(text=refs + commit.message.split("\n")[0], icon="DECORATE_ANIMATE") layout.label(text=refs + item.message.split("\n")[0], icon="DECORATE_ANIMATE")
layout.label(text=time.strftime("%c", time.localtime(commit.committed_date))) layout.label(text=time.strftime("%c", time.localtime(item.committed_date)))
def draw_filter(self, context, layout): def draw_filter(self, context, layout):
@@ -102,7 +102,6 @@ class MaterialsData:
if (style_name := s.Name) is not None if (style_name := s.Name) is not None
] ]
results = natsorted(results, key=lambda i: i[1]) results = natsorted(results, key=lambda i: i[1])
results.insert(0, ("-", "No Surface Style", ""))
return results return results
@classmethod @classmethod
@@ -210,14 +210,15 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_material_to_selected" bl_idname = "bim.assign_material_to_selected"
bl_label = "Assign Material To Selected" bl_label = "Assign Material To Selected"
bl_description = ( bl_description = (
"Assign currently selected material in Materials UI to the selected objects.\n\n" "Assign currently selected material in Materials UI to the selected objects.\n"
"ALT+CLICK to assign material as a usage." "Occurrences automatically get usages for layer/profile sets.\n\n"
"ALT+CLICK to assign without a usage."
) )
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material IFC ID") material: bpy.props.IntProperty(name="Material IFC ID")
assign_as_usage: bpy.props.BoolProperty( should_auto_assign_usage: bpy.props.BoolProperty(
name="Assign Material As A Usage", name="Auto Assign Usage",
default=False, default=True,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
@@ -230,25 +231,19 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event): def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt: if event.type == "LEFTMOUSE" and event.alt:
material_class = tool.Ifc.get().by_id(self.material).is_a() self.should_auto_assign_usage = False
if material_class not in ("IfcMaterialProfileSet", "IfcMaterialLayerSet"):
self.report({"ERROR"}, f"{material_class} cannot be assigned as a usage.")
return {"CANCELLED"}
self.assign_as_usage = True
return self.execute(context) return self.execute(context)
def _execute(self, context): def _execute(self, context):
material = tool.Ifc.get().by_id(self.material) material = tool.Ifc.get().by_id(self.material)
objects = tool.Blender.get_selected_objects() objects = tool.Blender.get_selected_objects()
material_type = material.is_a()
if self.assign_as_usage:
material_type += "Usage"
core.assign_material( core.assign_material(
tool.Ifc, tool.Ifc,
tool.Material, tool.Material,
material_type=material_type, material_type=material.is_a(),
objects=objects, objects=objects,
material=material, material=material,
should_auto_assign_usage=self.should_auto_assign_usage,
) )
@@ -722,7 +717,11 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
self.props.material_set_item_material = str(material_set_item.Material.id()) self.props.material_set_item_material = str(material_set_item.Material.id())
self.props.material_set_item_attributes.clear() self.props.material_set_item_attributes.clear()
bonsai.bim.helper.import_attributes(material_set_item, self.props.material_set_item_attributes) bonsai.bim.helper.import_attributes(
material_set_item,
self.props.material_set_item_attributes,
callback=self.import_attributes_callback,
)
if material_set_item.is_a("IfcMaterialProfile"): if material_set_item.is_a("IfcMaterialProfile"):
if material_set_item.Profile and material_set_item.Profile.ProfileName: if material_set_item.Profile and material_set_item.Profile.ProfileName:
@@ -730,6 +729,29 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
def import_attributes_callback(
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
) -> None | Literal[True]:
if data["type"] != "IfcMaterialLayer" or name != "IsVentilated" or not prop:
return None
# Keep null semantics unchanged on export, but avoid an empty UI selection.
prop.data_type = "enum"
prop.special_type = "LOGICAL"
prop.enum_items = json.dumps(("TRUE", "FALSE", "UNKNOWN"))
value = data[name]
if value == "UNKNOWN":
prop.enum_value = "UNKNOWN"
elif value is None:
# Keep visible default as FALSE, but preserve null semantics on save.
prop.enum_value = "FALSE"
prop.is_null = True
else:
prop.enum_value = "TRUE" if value else "FALSE"
return True
class DisableEditingMaterialSetItem(bpy.types.Operator): class DisableEditingMaterialSetItem(bpy.types.Operator):
bl_idname = "bim.disable_editing_material_set_item" bl_idname = "bim.disable_editing_material_set_item"
+11 -6
View File
@@ -118,12 +118,17 @@ class BIM_PT_materials(Panel):
row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id
row.operator("bim.disable_editing_material", text="", icon="CANCEL") row.operator("bim.disable_editing_material", text="", icon="CANCEL")
elif self.props.editing_material_type == "STYLE": elif self.props.editing_material_type == "STYLE":
row = self.layout.row(align=True) if MaterialsData.data["styles"]:
row.prop(self.props, "contexts", text="") row = self.layout.row(align=True)
prop_with_search(row, self.props, "styles", text="") row.prop(self.props, "contexts", text="")
row = self.layout.row(align=True) prop_with_search(row, self.props, "styles", text="")
row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK") row = self.layout.row(align=True)
row.operator("bim.disable_editing_material", text="", icon="CANCEL") row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK")
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
else:
row = self.layout.row(align=True)
row.label(text="No Styles Found")
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
class BIM_PT_object_material(Panel): class BIM_PT_object_material(Panel):
@@ -21,6 +21,11 @@ import bpy
from . import operator, prop, ui from . import operator, prop, ui
classes = ( classes = (
operator.ImportQuickFavorites,
operator.RemoveQuickFavoritesItem,
operator.MoveQuickFavoritesItem,
operator.AddQuickFavoritesItem,
operator.ConfirmQuickFavoriteOperator,
operator.DrawSystemArrows, operator.DrawSystemArrows,
operator.GetConnectedSystemElements, operator.GetConnectedSystemElements,
operator.IfcSverchokUseBonsaiFile, operator.IfcSverchokUseBonsaiFile,
@@ -28,8 +33,12 @@ classes = (
operator.SetOverrideColour, operator.SetOverrideColour,
operator.SnapSpacesTogether, operator.SnapSpacesTogether,
operator.SplitAlongEdge, operator.SplitAlongEdge,
prop.QuickFavoriteEnumItem,
prop.QuickFavoriteProperty,
prop.QuickFavoritesItem,
prop.BIMMiscProperties, prop.BIMMiscProperties,
ui.BIM_PT_misc_utilities, ui.BIM_PT_misc_utilities,
ui.BIM_PT_quick_favorites_manager,
) )
+48
View File
@@ -0,0 +1,48 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import bpy
def refresh() -> None:
QuickFavoritesData.is_loaded = False
class QuickFavoritesData:
data: dict[str, Any] = {}
is_loaded = False
@classmethod
def load(cls) -> None:
cls.data = {
"operators": cls.operators(),
}
cls.is_loaded = True
@classmethod
def operators(cls) -> list[str]:
items: list[str] = []
for module_name in dir(bpy.ops):
module = getattr(bpy.ops, module_name)
for op_name in dir(module):
op = getattr(module, op_name)
bl_label = op.get_rna_type().name
items.append(f"{module_name}.{op_name} - {bl_label}")
return items
+152 -3
View File
@@ -30,6 +30,9 @@ import bonsai.core.misc as core
import bonsai.core.root import bonsai.core.root
import bonsai.tool as tool import bonsai.tool as tool
if TYPE_CHECKING:
from bpy.stub_internal import rna_enums
class SetOverrideColour(bpy.types.Operator): class SetOverrideColour(bpy.types.Operator):
bl_idname = "bim.set_override_colour" bl_idname = "bim.set_override_colour"
@@ -41,10 +44,11 @@ class SetOverrideColour(bpy.types.Operator):
return context.selected_objects return context.selected_objects
def execute(self, context): def execute(self, context):
props = tool.Misc.get_misc_props()
for obj in context.selected_objects: for obj in context.selected_objects:
obj.color = context.scene.BIMMiscProperties.override_colour obj.color = props.override_colour
area = next(area for area in context.screen.areas if area.type == "VIEW_3D") assert (space := tool.Blender.get_view3d_space())
area.spaces[0].shading.color_type = "OBJECT" space.shading.color_type = "OBJECT"
return {"FINISHED"} return {"FINISHED"}
@@ -351,6 +355,151 @@ class DrawSystemArrows(bpy.types.Operator, tool.Ifc.Operator):
return matrix return matrix
class ConfirmQuickFavoriteOperator(bpy.types.Operator):
bl_idname = "bim.confirm_quick_favorite_operator"
bl_label = "Confirm Operator"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
index: int
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Misc.get_misc_props()
fav = props.quick_favorites[self.index]
rna = fav.get_searched_operator()
if rna is None:
self.report({"INFO"}, "No operator entered for search.")
return {"CANCELLED"}
fav.operator_id = tool.Blender.operator_idname_to_py(rna.identifier)
fav.label = rna.name
fav.properties.clear()
has_skipped = False
for p in rna.properties:
# skip silently, e.g. `rna_type` is a PointerProperty
if isinstance(p, bpy.types.PointerProperty):
continue
if isinstance(p, (bpy.types.FloatProperty, bpy.types.BoolProperty, bpy.types.IntProperty)) and p.is_array:
print(f"Array property '{p.identifier}' is not supported, skipping.")
has_skipped = True
continue
item = fav.properties.add()
item.name = p.identifier
item.display_name = p.name
if isinstance(p, bpy.types.FloatProperty):
item.value_prop = "float_value"
item.float_value = p.default
elif isinstance(p, bpy.types.BoolProperty):
item.value_prop = "bool_value"
item.bool_value = p.default
elif isinstance(p, bpy.types.IntProperty):
item.value_prop = "int_value"
item.int_value = p.default
elif isinstance(p, bpy.types.EnumProperty):
item.value_prop = "enum_value"
item.set_enum_items([(e.identifier, e.name, e.description) for e in p.enum_items])
item.enum_value = p.default
elif isinstance(p, bpy.types.StringProperty):
item.value_prop = "string_value"
item.string_value = p.default
else:
print(f"Unhandled property type {type(p).__name__} for '{p.identifier}', skipping.")
has_skipped = True
if has_skipped:
self.report({"WARNING"}, "Some properties were skipped, see the system console for details.")
return {"FINISHED"}
class ImportQuickFavorites(bpy.types.Operator):
bl_idname = "bim.import_quick_favorites"
bl_label = "Import Quick Favorites"
bl_description = "Import operators from Blender's Quick Favorites menu, including their configured properties"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Misc.get_misc_props()
props.quick_favorites.clear()
has_missing_props = False
for i, qf in enumerate(tool.Misc.QuickFavorites.get_quick_favorites()):
fav = props.quick_favorites.add()
fav.label = qf.ui_name
fav.search = qf.op_idname_py
bpy.ops.bim.confirm_quick_favorite_operator(index=i)
fav.label = qf.ui_name or fav.label
for prop in fav.properties:
prop.is_active = prop.name in qf.props
for key, value in qf.props.items():
if key not in fav.properties:
print(f"Property '{key}' not found in operator '{qf.op_idname_py}'.")
has_missing_props = True
continue
item = fav.properties[key]
item.set_value(value)
if has_missing_props:
self.report(
{"WARNING"}, "Some properties were not found during import, see the system console for details."
)
return {"FINISHED"}
class MoveQuickFavoritesItem(bpy.types.Operator):
bl_idname = "bim.move_quick_favorites_item"
bl_label = "Move Quick Favorites Item"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
direction: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[("UP", "Up", ""), ("DOWN", "Down", "")]
)
if TYPE_CHECKING:
index: int
direction: Literal["UP", "DOWN"]
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Misc.get_misc_props()
total = len(props.quick_favorites)
new_index = self.index - 1 if self.direction == "UP" else self.index + 1
if 0 <= new_index < total:
props.quick_favorites.move(self.index, new_index)
return {"FINISHED"}
class RemoveQuickFavoritesItem(bpy.types.Operator):
bl_idname = "bim.remove_quick_favorites_item"
bl_label = "Remove Quick Favorites Item"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
index: int
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Misc.get_misc_props()
props.quick_favorites.remove(self.index)
return {"FINISHED"}
class AddQuickFavoritesItem(bpy.types.Operator):
bl_idname = "bim.add_quick_favorites_item"
bl_label = "Add Quick Favorites Item"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Misc.get_misc_props()
fav = props.quick_favorites.add()
fav.search = "bim.select_query_elements"
index = len(props.quick_favorites) - 1
bpy.ops.bim.confirm_quick_favorite_operator(index=index)
fav.properties["query"].string_value = "IfcWall"
return {"FINISHED"}
class IfcSverchokUseBonsaiFile(bpy.types.Operator, tool.Ifc.Operator): class IfcSverchokUseBonsaiFile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.ifcsverchok_use_bonsai_file" bl_idname = "bim.ifcsverchok_use_bonsai_file"
bl_label = "Use Bonsai IFC File" bl_label = "Use Bonsai IFC File"
+123 -2
View File
@@ -16,19 +16,140 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from typing import TYPE_CHECKING, Any, Literal, cast, get_args
import bpy
from bpy.props import ( from bpy.props import (
BoolProperty,
CollectionProperty,
EnumProperty,
FloatProperty,
FloatVectorProperty, FloatVectorProperty,
IntProperty, IntProperty,
StringProperty,
) )
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bonsai.bim.module.misc.data import QuickFavoritesData
QuickFavoriteValueType = Literal["float_value", "bool_value", "int_value", "string_value", "enum_value"]
class QuickFavoriteEnumItem(PropertyGroup):
name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration]
display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration]
description: StringProperty(name="Description", default="") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
name: str
display_name: str
description: str
def get_enum_items(self: "QuickFavoriteProperty", context: bpy.types.Context | None) -> list[tuple[str, str, str]]:
return [(item.name, item.display_name, item.description) for item in self.enum_items]
class QuickFavoriteProperty(PropertyGroup):
name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration]
display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration]
value_prop: EnumProperty( # pyright: ignore[reportRedeclaration]
name="Value Prop",
items=tuple((v, v, "") for v in get_args(QuickFavoriteValueType)),
)
string_value: StringProperty(name="String Value", default="") # pyright: ignore[reportRedeclaration]
float_value: FloatProperty(name="Float Value", default=0.0) # pyright: ignore[reportRedeclaration]
int_value: IntProperty(name="Int Value", default=0) # pyright: ignore[reportRedeclaration]
bool_value: BoolProperty(name="Bool Value", default=False) # pyright: ignore[reportRedeclaration]
enum_value: EnumProperty(name="Enum Value", items=get_enum_items) # pyright: ignore[reportRedeclaration]
enum_items: CollectionProperty(type=QuickFavoriteEnumItem) # pyright: ignore[reportRedeclaration]
is_active: BoolProperty( # pyright: ignore[reportRedeclaration]
name="Is Active",
description="Only active properties will be added to the operator when invoked from Quick Favorites",
default=False,
)
def set_value(self, value: Any) -> None:
setattr(self, self.value_prop, value)
def set_enum_items(self, items: list[tuple[str, str, str]]) -> None:
self.enum_items.clear()
for identifier, name, description in items:
item = self.enum_items.add()
item.name = identifier
item.display_name = name
item.description = description
if TYPE_CHECKING:
name: str
display_name: str
value_prop: QuickFavoriteValueType
string_value: str
float_value: float
int_value: int
bool_value: bool
enum_value: str
enum_items: bpy.types.bpy_prop_collection_idprop[QuickFavoriteEnumItem]
is_active: bool
def get_operator_suggestions(self: "QuickFavoritesItem", context: bpy.types.Context, edit_text: str) -> list[str]:
if not QuickFavoritesData.is_loaded:
QuickFavoritesData.load()
return QuickFavoritesData.data["operators"]
class QuickFavoritesItem(PropertyGroup):
is_expanded: BoolProperty(name="Is Expanded", default=False) # pyright: ignore[reportRedeclaration]
search: StringProperty( # pyright: ignore[reportRedeclaration]
name="Search",
default="",
search=get_operator_suggestions,
# Resetting `search_options`, allowing users only to use suggestions.
search_options=set(),
)
properties: CollectionProperty(type=QuickFavoriteProperty) # pyright: ignore[reportRedeclaration]
operator_id: StringProperty( # pyright: ignore[reportRedeclaration]
name="Operator ID",
default="",
)
label: StringProperty( # pyright: ignore[reportRedeclaration]
name="Label",
description="Label that will be used in Quick Favorites for this operator",
default="",
)
def get_searched_operator(self) -> bpy.types.Struct | None:
if not self.search:
return None
search_label = self.search
name = search_label.split(" - ", 1)[0]
module, func = name.split(".", 1)
op = getattr(getattr(bpy.ops, module), func)
rna = cast(bpy.types.Struct, op.get_rna_type())
return rna
if TYPE_CHECKING:
is_expanded: bool
search: str
"""Internal property set when confirming results of the search field"""
properties: bpy.types.bpy_prop_collection_idprop[QuickFavoriteProperty]
operator_id: str
label: str
class BIMMiscProperties(PropertyGroup): class BIMMiscProperties(PropertyGroup):
total_storeys: IntProperty( total_storeys: IntProperty( # pyright: ignore[reportRedeclaration]
name="Total Storeys", name="Total Storeys",
description="Number of storeys above object's storey to take into account for resizing", description="Number of storeys above object's storey to take into account for resizing",
default=1, default=1,
) )
override_colour: FloatVectorProperty( override_colour: FloatVectorProperty( # pyright: ignore[reportRedeclaration]
name="Override Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4 name="Override Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4
) )
quick_favorites: CollectionProperty(type=QuickFavoritesItem) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
total_storeys: int
override_colour: tuple[float, float, float, float]
quick_favorites: bpy.types.bpy_prop_collection_idprop[QuickFavoritesItem]
+73 -1
View File
@@ -18,6 +18,8 @@
import bpy import bpy
import bonsai.tool as tool
class BIM_PT_misc_utilities(bpy.types.Panel): class BIM_PT_misc_utilities(bpy.types.Panel):
bl_idname = "BIM_PT_misc_utilities" bl_idname = "BIM_PT_misc_utilities"
@@ -30,7 +32,8 @@ class BIM_PT_misc_utilities(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
props = context.scene.BIMMiscProperties assert layout
props = tool.Misc.get_misc_props()
row = layout.split(factor=0.2, align=True) row = layout.split(factor=0.2, align=True)
row.prop(props, "override_colour", text="") row.prop(props, "override_colour", text="")
row.operator("bim.set_override_colour") row.operator("bim.set_override_colour")
@@ -56,3 +59,72 @@ class BIM_PT_misc_utilities(bpy.types.Panel):
row.operator("bim.disable_editing_sketch_extrusion_profile", text="", icon="CANCEL") row.operator("bim.disable_editing_sketch_extrusion_profile", text="", icon="CANCEL")
row = layout.row() row = layout.row()
row.operator("bim.import_plot", text="Import Plot Coordinates", icon="FILE_FOLDER") row.operator("bim.import_plot", text="Import Plot Coordinates", icon="FILE_FOLDER")
class BIM_PT_quick_favorites_manager(bpy.types.Panel):
bl_idname = "BIM_PT_quick_favorites_manager"
bl_label = "Quick Favorites Manager"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "output"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_tab_sandbox"
def draw(self, context):
layout = self.layout
assert layout
props = tool.Misc.get_misc_props()
row = layout.row(align=True)
row.label(text="Quick Favorites:")
row.operator("bim.add_quick_favorites_item", text="", icon="ADD")
row.operator("bim.import_quick_favorites", text="", icon="BLENDER")
op = row.operator("bim.show_description", text="", icon="INFO")
op.attr_name = "Quick Favorites Manager"
op.description = (
"Blender does not support editing Quick Favorites natively. "
"This manager allows you to load existing Quick Favorites operators, "
"configure their properties and labels, and re-add them to the menu with customized settings."
)
for fav in props.quick_favorites:
if fav.operator_id:
row = layout.row()
op = row.operator(fav.operator_id, text=fav.label)
for item in fav.properties:
if item.is_active:
setattr(op, item.name, getattr(item, item.value_prop))
layout.separator()
for i, fav in enumerate(props.quick_favorites):
box = layout.box()
row = box.row(align=True)
row.prop(fav, "is_expanded", text="", icon="TRIA_DOWN" if fav.is_expanded else "TRIA_RIGHT", emboss=False)
row.prop(fav, "label", text="")
if i > 0:
up = row.operator("bim.move_quick_favorites_item", text="", icon="TRIA_UP")
up.index = i
up.direction = "UP"
if i < len(props.quick_favorites) - 1:
down = row.operator("bim.move_quick_favorites_item", text="", icon="TRIA_DOWN")
down.index = i
down.direction = "DOWN"
row.operator("bim.remove_quick_favorites_item", text="", icon="X").index = i
if not fav.is_expanded:
continue
row = box.row(align=True)
row.prop(fav, "search", text="")
row.operator("bim.confirm_quick_favorite_operator", text="", icon="VIEWZOOM").index = i
if not fav.operator_id:
continue
layout.separator()
if fav.properties:
box.label(text="Properties:")
prop_box = box.box()
for item in fav.properties:
row = prop_box.row(align=True)
row.prop(item, item.value_prop, text=item.display_name)
row.prop(item, "is_active", text="", icon="RADIOBUT_ON" if item.is_active else "RADIOBUT_OFF")
else:
box.label(text="No Properties.")
@@ -19,7 +19,7 @@
from __future__ import annotations from __future__ import annotations
import math import math
from math import cos, radians, sin, tan from math import cos, pi, radians, sin, tan
from typing import Any, Literal from typing import Any, Literal
import blf import blf
@@ -27,6 +27,10 @@ import bmesh
import bpy import bpy
import gpu import gpu
import ifcopenshell import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import mathutils import mathutils
from bpy.types import SpaceView3D from bpy.types import SpaceView3D
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
@@ -35,6 +39,7 @@ from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_circle_2d from gpu_extras.presets import draw_circle_2d
from mathutils import Matrix, Quaternion, Vector from mathutils import Matrix, Quaternion, Vector
import bonsai.core.geometry
import bonsai.tool as tool import bonsai.tool as tool
from bonsai.bim.module.drawing.helper import format_distance from bonsai.bim.module.drawing.helper import format_distance
@@ -1566,7 +1571,7 @@ class ProductDecorator:
obj_type, obj_type,
representation, representation,
) )
context.view_layer.update() bpy.context.view_layer.update()
break break
translate_mouse = Matrix.Translation(mouse_point) translate_mouse = Matrix.Translation(mouse_point)
+1 -1
View File
@@ -82,7 +82,7 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
class BIM_OT_add_object(Operator, tool.Ifc.Operator): class BIM_OT_add_object(Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_grid" bl_idname = "bim.add_grid"
bl_label = "Grid" bl_label = "Grid"
bl_description = "Add IfcGrid." bl_description = "Add IfcGrid."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
+2 -2
View File
@@ -227,7 +227,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001) is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001) is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23): if not all([is_parallel12, is_parallel13, is_parallel21, is_parallel23]):
fitting_type = "WYE" fitting_type = "WYE"
if not fitting_type: if not fitting_type:
@@ -903,7 +903,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0) start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0) end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
radius: bpy.props.FloatProperty( radius: bpy.props.FloatProperty(
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0 name="Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
) )
def _execute(self, context): def _execute(self, context):
+29 -138
View File
@@ -151,29 +151,11 @@ class FilledOpeningGenerator:
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
) )
assert representation assert representation
representation = ifcopenshell.util.representation.resolve_representation(representation)
# Check if mapped representation - PRESERVE the mapping structure else:
if ( representation = self.generate_opening_from_filling(
representation.RepresentationType == "MappedRepresentation" filling, filling_obj, opening_thickness_si=opening_thickness_si
and len(representation.Items) == 1 )
and representation.Items[0].is_a("IfcMappedItem")
):
# Store the existing RepresentationMap to reuse it
existing_mapping_source = representation.Items[0].MappingSource
reuse_mapped_representation = True
else:
representation = ifcopenshell.util.representation.resolve_representation(representation)
if not reuse_mapped_representation:
# Check for library template before generating from filling
template_rep = self.get_opening_template_from_type(filling)
if template_rep:
representation = template_rep
else:
representation = self.generate_opening_from_filling(
filling, filling_obj, opening_thickness_si=opening_thickness_si
)
# Create mapped representation # Create mapped representation
if reuse_mapped_representation: if reuse_mapped_representation:
@@ -247,109 +229,38 @@ class FilledOpeningGenerator:
voided_element = opening.VoidsElements[0].RelatingBuildingElement voided_element = opening.VoidsElements[0].RelatingBuildingElement
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW") opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
# ALWAYS preserve the existing opening representation (Tessellation, SweptSolid, etc.)
preserved_representation = None
if opening_rep:
if (
opening_rep.RepresentationType == "MappedRepresentation"
and len(opening_rep.Items) == 1
and opening_rep.Items[0].is_a("IfcMappedItem")
):
# For mapped representations, copy the underlying representation
preserved_representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
opening_rep.Items[0].MappingSource.MappedRepresentation,
exclude=["IfcGeometricRepresentationContext"],
)
else:
# For direct representations (non-mapped), copy them too
preserved_representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), opening_rep, exclude=["IfcGeometricRepresentationContext"]
)
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep) ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep) ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling) existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
# Priority order for choosing representation:
# 1. Existing occurrence with MappedRepresentation (preserve mapping!)
# 2. Library template with Tessellation
# 3. Preserved representation from old opening (maintain user's work)
# 4. Generate from filling (last resort)
representation_to_use = None
reuse_mapped_representation = False
existing_mapping_source = None
if existing_opening_occurrence: if existing_opening_occurrence:
representation = ifcopenshell.util.representation.get_representation( representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
) )
representation = ifcopenshell.util.representation.resolve_representation(representation)
if ( mapped_representation = ifcopenshell.api.geometry.map_representation(
representation tool.Ifc.get(), representation=representation
and representation.RepresentationType == "MappedRepresentation" )
and len(representation.Items) == 1 ifcopenshell.api.geometry.assign_representation(
and representation.Items[0].is_a("IfcMappedItem") tool.Ifc.get(), product=opening, representation=mapped_representation
): )
# PRESERVE the mapped structure - reuse the same RepresentationMap else:
existing_mapping_source = representation.Items[0].MappingSource
reuse_mapped_representation = True
else:
representation_to_use = ifcopenshell.util.representation.resolve_representation(representation)
if not representation_to_use and not reuse_mapped_representation:
template_rep = self.get_opening_template_from_type(filling)
if template_rep and template_rep.RepresentationType == "Tessellation":
representation_to_use = template_rep
if not representation_to_use and not reuse_mapped_representation and preserved_representation:
representation_to_use = preserved_representation
if not representation_to_use and not reuse_mapped_representation:
opening_obj = tool.Ifc.get_object(opening) opening_obj = tool.Ifc.get_object(opening)
if opening_obj: if opening_obj:
tool.Ifc.unlink(element=opening) tool.Ifc.unlink(element=opening)
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True) tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
filling_obj = tool.Ifc.get_object(filling) filling_obj = tool.Ifc.get_object(filling)
representation_to_use = self.generate_opening_from_filling(filling, filling_obj) representation = self.generate_opening_from_filling(filling, filling_obj)
# Create the mapped representation
if reuse_mapped_representation:
# Reuse existing RepresentationMap - don't create a new one!
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
new_mapped_item = tool.Ifc.get().create_entity(
"IfcMappedItem",
MappingSource=existing_mapping_source,
MappingTarget=tool.Ifc.get().create_entity(
"IfcCartesianTransformationOperator3D",
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 1.0, 0.0)),
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
Scale=1.0,
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
),
)
mapped_representation = tool.Ifc.get().create_entity(
"IfcShapeRepresentation",
ContextOfItems=context,
RepresentationIdentifier="Body",
RepresentationType="MappedRepresentation",
Items=[new_mapped_item],
)
else:
mapped_representation = ifcopenshell.api.geometry.map_representation( mapped_representation = ifcopenshell.api.geometry.map_representation(
tool.Ifc.get(), representation=representation_to_use tool.Ifc.get(), representation=representation
)
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(), product=opening, representation=mapped_representation
) )
ifcopenshell.api.geometry.assign_representation( # update voided object representation or all it's parts if it's an aggregate
tool.Ifc.get(), product=opening, representation=mapped_representation
)
# update voided object representation...
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element] voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
for voided_element in voided_elements: for voided_element in voided_elements:
voided_obj = tool.Ifc.get_object(voided_element) voided_obj = tool.Ifc.get_object(voided_element)
@@ -363,36 +274,6 @@ class FilledOpeningGenerator:
representation=representation, representation=representation,
) )
def get_opening_template_from_type(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""
Check if the filling's type has a stored opening template from library import.
"""
element_type = ifcopenshell.util.element.get_type(filling)
if not element_type:
return None
desc = element_type.Description
if not desc or "||BonsaiOpeningTemplate:" not in desc:
return None
# Extract template ID
marker = desc.split("||BonsaiOpeningTemplate:")[-1]
template_id = int(marker.split("||")[0])
try:
template_rep = tool.Ifc.get().by_id(template_id)
# Make a copy so we don't reuse the same representation instance
copied = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), template_rep, exclude=["IfcGeometricRepresentationContext"]
)
return copied
except:
return None
def generate_opening_from_filling( def generate_opening_from_filling(
self, self,
filling: ifcopenshell.entity_instance, filling: ifcopenshell.entity_instance,
@@ -659,6 +540,16 @@ class AddBoolean(Operator, tool.Ifc.Operator):
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator) booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
rep_obj = tool.Geometry.get_geometry_props().representation_obj rep_obj = tool.Geometry.get_geometry_props().representation_obj
if booleans:
# Users typically select two top-level items and expect the
# operand to be absorbed into the boolean, not remain as a
# standalone item alongside it.
representation = tool.Geometry.get_active_representation(rep_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
second_items_set = set(second_items)
new_items = [i for i in representation.Items if i not in second_items_set]
if new_items:
representation.Items = new_items
rep_element = tool.Ifc.get_entity(rep_obj) rep_element = tool.Ifc.get_entity(rep_obj)
tool.Model.mark_manual_booleans(rep_element, booleans) tool.Model.mark_manual_booleans(rep_element, booleans)
tool.Geometry.reload_representation(rep_obj) tool.Geometry.reload_representation(rep_obj)
@@ -421,7 +421,7 @@ class PolylineOperator:
tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state) tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state)
tool.Blender.update_viewport() tool.Blender.update_viewport()
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
def set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None: def set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
props = tool.Model.get_model_props() props = tool.Model.get_model_props()
@@ -461,6 +461,7 @@ class PolylineOperator:
self.tool_state.axis_method = None self.tool_state.axis_method = None
self.tool_state.plane_method = None self.tool_state.plane_method = None
self.tool_state.mode = "Mouse" self.tool_state.mode = "Mouse"
tool.Raycast.clear_snap_objs()
self.visible_objs = tool.Raycast.get_visible_objects(context) self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs: for obj in self.visible_objs:
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj): if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
@@ -694,10 +694,14 @@ def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[
new_settings = settings.copy() new_settings = settings.copy()
new_settings["context"] = box_context new_settings["context"] = box_context
new_box = ifcopenshell.api.geometry.add_representation(ifc_file, should_run_listeners=False, **new_settings) new_box = ifcopenshell.api.geometry.add_representation(
ifc_file,
should_run_listeners=False, # ty:ignore[unknown-argument]
**new_settings,
)
ifcopenshell.api.geometry.assign_representation( ifcopenshell.api.geometry.assign_representation(
ifc_file, ifc_file,
should_run_listeners=False, should_run_listeners=False, # ty:ignore[unknown-argument]
product=product, product=product,
representation=new_box, representation=new_box,
) )
+40 -16
View File
@@ -18,7 +18,7 @@
import copy import copy
from math import atan2, degrees, pi, radians from math import atan2, degrees, pi, radians
from typing import Any, Literal, Optional, Union from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import bpy import bpy
import ifcopenshell import ifcopenshell
@@ -49,7 +49,7 @@ ProfileFrom2PointsReturn = Union[dict[str, Any], None]
class DumbProfileGenerator: class DumbProfileGenerator:
def __init__(self, relating_type): def __init__(self, relating_type: ifcopenshell.entity_instance):
self.relating_type = relating_type self.relating_type = relating_type
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -201,7 +201,7 @@ class DumbProfileGenerator:
class DumbProfileRegenerator: class DumbProfileRegenerator:
def regenerate_from_profile_def(self, profile): def regenerate_from_profile_def(self, profile: ifcopenshell.entity_instance) -> None:
self.file = tool.Ifc.get() self.file = tool.Ifc.get()
objs = [] objs = []
if not profile: if not profile:
@@ -221,7 +221,7 @@ class DumbProfileRegenerator:
for element in self.get_element_types_using_profile(profile): for element in self.get_element_types_using_profile(profile):
tool.Model.mark_thumbnail_for_update(element) tool.Model.mark_thumbnail_for_update(element)
def regenerate_from_profile(self, usecase_path, ifc_file, settings): def regenerate_from_profile(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
self.file = ifc_file self.file = ifc_file
objs = [] objs = []
profile = settings["profile"].Profile profile = settings["profile"].Profile
@@ -233,7 +233,7 @@ class DumbProfileRegenerator:
objs.append(obj) objs.append(obj)
DumbProfileRecalculator().recalculate(objs) DumbProfileRecalculator().recalculate(objs)
def get_elements_using_profile(self, profile): def get_elements_using_profile(self, profile: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
results = [] results = []
profile_sets = [ profile_sets = [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile") mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
@@ -252,7 +252,9 @@ class DumbProfileRegenerator:
results.extend(rel.RelatedObjects) results.extend(rel.RelatedObjects)
return results return results
def get_element_types_using_profile(self, profile): def get_element_types_using_profile(
self, profile: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
results = [] results = []
profile_sets = [ profile_sets = [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile") mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
@@ -269,12 +271,18 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.extend_profile" bl_idname = "bim.extend_profile"
bl_label = "Extend Profile" bl_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty() join_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[("-", "Unjoin", ""), ("L", "L", ""), ("V", "V", ""), ("T", "T", "")],
default="-",
)
if TYPE_CHECKING:
join_type: Literal["-", "L", "V", "T"]
def _execute(self, context): def _execute(self, context):
selected_objs = context.selected_objects selected_objs = context.selected_objects
joiner = DumbProfileJoiner() joiner = DumbProfileJoiner()
if not self.join_type: if self.join_type == "-":
for obj in selected_objs: for obj in selected_objs:
joiner.unjoin(obj) joiner.unjoin(obj)
return {"FINISHED"} return {"FINISHED"}
@@ -626,11 +634,15 @@ class DumbProfileJoiner:
if connection1 == "ATEND": if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal: if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane) plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[1] = intersect self.body[1] = intersect
else: else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False) plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1) max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim)) self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -673,11 +685,15 @@ class DumbProfileJoiner:
elif connection1 == "ATSTART": elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal: if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane) plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[0] = intersect self.body[0] = intersect
else: else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False) plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1) max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim)) self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -721,7 +737,9 @@ class DumbProfileJoiner:
if connection1 == "ATEND": if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal: if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane) plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[1] = intersect self.body[1] = intersect
else: else:
plane = self.get_profile_plane( plane = self.get_profile_plane(
@@ -729,7 +747,9 @@ class DumbProfileJoiner:
furthest_plane if is_relating else closest_plane, furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True, z_inwards=False if is_relating else True,
) )
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1) max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim)) self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append( self.clippings.append(
@@ -742,7 +762,9 @@ class DumbProfileJoiner:
elif connection1 == "ATSTART": elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal: if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane) plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[0] = intersect self.body[0] = intersect
else: else:
plane = self.get_profile_plane( plane = self.get_profile_plane(
@@ -750,7 +772,9 @@ class DumbProfileJoiner:
furthest_plane if is_relating else closest_plane, furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True, z_inwards=False if is_relating else True,
) )
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1) max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim)) self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append( self.clippings.append(
@@ -733,6 +733,9 @@ class BIMStairProperties(PropertyGroup):
class BIMSverchokProperties(PropertyGroup): class BIMSverchokProperties(PropertyGroup):
node_group: bpy.props.PointerProperty(name="Node Group", type=NodeTree) node_group: bpy.props.PointerProperty(name="Node Group", type=NodeTree)
if TYPE_CHECKING:
node_group: bpy.types.NodeTree | None
def window_type_prop_update(self, context): def window_type_prop_update(self, context):
number_of_panels, panels_data = self.window_types_panels[self.window_type] number_of_panels, panels_data = self.window_types_panels[self.window_type]
@@ -31,7 +31,7 @@ import bonsai.tool as tool
def update_sverchok_modifier(context): def update_sverchok_modifier(context):
obj = context.active_object obj = context.active_object
props = obj.BIMSverchokProperties props = tool.Model.get_sverchok_props(obj)
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
psets = ifcopenshell.util.element.get_psets(element) psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("BBIM_Sverchok", None) pset = psets.get("BBIM_Sverchok", None)
@@ -69,10 +69,10 @@ class CreateNewSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
def _execute(self, context): def _execute(self, context):
import sverchok import sverchok.ui.sv_temporal_viewers
obj = context.active_object obj = context.active_object
props = obj.BIMSverchokProperties props = tool.Model.get_sverchok_props(obj)
node_group = bpy.data.node_groups.new("IfcNodeTree", type="SverchCustomTreeType") node_group = bpy.data.node_groups.new("IfcNodeTree", type="SverchCustomTreeType")
plane = node_group.nodes.new(type="SvPlaneNodeMk3") plane = node_group.nodes.new(type="SvPlaneNodeMk3")
@@ -96,7 +96,7 @@ class DeleteSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
obj = context.active_object obj = context.active_object
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
props = obj.BIMSverchokProperties props = tool.Model.get_sverchok_props(obj)
bpy.data.node_groups.remove(props.node_group) bpy.data.node_groups.remove(props.node_group)
return {"FINISHED"} return {"FINISHED"}
@@ -113,7 +113,8 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
def invoke(self, context, event): def invoke(self, context, event):
if not context.active_object.BIMSverchokProperties.node_group: props = tool.Model.get_sverchok_props(context.active_object)
if not props.node_group:
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
return self._execute(context) return self._execute(context)
@@ -124,7 +125,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
obj = context.active_object obj = context.active_object
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
props = obj.BIMSverchokProperties props = tool.Model.get_sverchok_props(obj)
node_group = props.node_group node_group = props.node_group
if node_group: if node_group:
@@ -192,11 +193,11 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
filename_ext = ".json" filename_ext = ".json"
def _execute(self, context): def _execute(self, context):
import sverchok import sverchok.utils.sv_json_import
importer = sverchok.utils.sv_json_import.JSONImporter.init_from_path(self.filepath) importer = sverchok.utils.sv_json_import.JSONImporter.init_from_path(self.filepath)
obj = context.active_object obj = context.active_object
props = obj.BIMSverchokProperties props = tool.Model.get_sverchok_props(obj)
node_group = context.scene.io_panel_properties.import_tree node_group = context.scene.io_panel_properties.import_tree
if not node_group: if not node_group:
@@ -231,10 +232,10 @@ class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
compress: bpy.props.BoolProperty() compress: bpy.props.BoolProperty()
def _execute(self, context): def _execute(self, context):
import sverchok import sverchok.utils.sv_json_export
obj = context.active_object obj = context.active_object
props = obj.BIMSverchokProperties props = tool.Model.get_sverchok_props(obj)
ng = props.node_group ng = props.node_group
destination_path = self.filepath destination_path = self.filepath
if not destination_path.lower().endswith(".json"): if not destination_path.lower().endswith(".json"):
@@ -273,7 +274,8 @@ class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
return {"FINISHED"} return {"FINISHED"}
def draw(self, context): def draw(self, context):
graph_name = context.active_object.BIMSverchokProperties.node_group.name props = tool.Model.get_sverchok_props(context.active_object)
graph_name = props.node_group.name
self.layout.label(text=f'Save node tree "{graph_name}" into json:') self.layout.label(text=f'Save node tree "{graph_name}" into json:')
col = self.layout.column(heading="Options") # new syntax in >= 2.90 col = self.layout.column(heading="Options") # new syntax in >= 2.90
+2 -2
View File
@@ -31,11 +31,11 @@ def calculate_quantities(usecase_path, ifc_file: ifcopenshell.file, settings):
return return
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask")) task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
qto = ifcopenshell.api.pset.add_qto( qto = ifcopenshell.api.pset.add_qto(
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities" ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities" # ty:ignore[unknown-argument]
) )
ifcopenshell.api.pset.edit_qto( ifcopenshell.api.pset.edit_qto(
ifc_file, ifc_file,
should_run_listeners=False, should_run_listeners=False, # ty:ignore[unknown-argument]
qto=qto, qto=qto,
properties={ properties={
"StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days, "StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days,
+1 -1
View File
@@ -360,7 +360,7 @@ class BIM_PT_sverchok(bpy.types.Panel):
self.layout.label(text="Requires Sverchok Add-on", icon="ERROR") self.layout.label(text="Requires Sverchok Add-on", icon="ERROR")
return return
props = context.active_object.BIMSverchokProperties props = tool.Model.get_sverchok_props(context.active_object)
self.layout.prop_search(props, "node_group", bpy.data, "node_groups") self.layout.prop_search(props, "node_group", bpy.data, "node_groups")
self.layout.operator("bim.create_new_sverchok_graph", icon="ADD") self.layout.operator("bim.create_new_sverchok_graph", icon="ADD")
@@ -1268,27 +1268,6 @@ class DumbWallJoiner:
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2) bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
return wall2 return wall2
def join_Z(self, wall1, slab2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
for rel in element1.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
ifcopenshell.api.geometry.disconnect_element(
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element1,
)
ifcopenshell.api.geometry.connect_element(
tool.Ifc.get(),
relating_element=element2,
related_element=element1,
description="TOP",
)
tool.Model.recreate_wall(element1, wall1)
def set_axis(self, wall, p1, p2): def set_axis(self, wall, p1, p2):
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW") axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -1333,29 +1312,6 @@ class DumbWallJoiner:
self.set_axis(element1, p1, p2) self.set_axis(element1, p1, p2)
tool.Model.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)
element2 = tool.Ifc.get_entity(wall2)
axis1 = tool.Model.get_wall_axis(wall1)
axis2 = tool.Model.get_wall_axis(wall2)
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
if intersect:
intersect, _ = intersect
else:
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
ifcopenshell.api.geometry.connect_path(
tool.Ifc.get(),
related_element=element1,
relating_element=element2,
relating_connection="ATPATH",
related_connection=connection,
description="BUTT",
)
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None: def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
wall1 = tool.Ifc.get_entity(obj1) wall1 = tool.Ifc.get_entity(obj1)
wall2 = tool.Ifc.get_entity(obj2) wall2 = tool.Ifc.get_entity(obj2)
@@ -943,7 +943,7 @@ class EditObjectUI:
if "LAYER2" in AuthoringData.data["selected_material_usages"]: if "LAYER2" in AuthoringData.data["selected_material_usages"]:
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator( add_layout_hotkey_operator(
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_to_underside.__doc__, ui_context cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_walls_to_underside.__doc__, ui_context
) )
if AuthoringData.data["is_flippable_element"]: if AuthoringData.data["is_flippable_element"]:
@@ -101,7 +101,7 @@ class NestDecorator:
cls.is_installed = False cls.is_installed = False
def dotted_line_shader(self): def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out.smooth("FLOAT", "v_ArcLength") vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo() shader_info = gpu.types.GPUShaderCreateInfo()
@@ -215,8 +215,6 @@ class NestDecorator:
self.draw_batch("LINES", line_z, color, [(0, 1)]) self.draw_batch("LINES", line_z, color, [(0, 1)])
else: else:
self.draw_batch("POINTS", [location], color) self.draw_batch("POINTS", [location], color)
# if context.scene.BIMNestProperties.in_aggregate_mode:
# return
components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(nest)) components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(nest))
components_objs = [tool.Ifc.get_object(p) for p in components] components_objs = [tool.Ifc.get_object(p) for p in components]
components_objs.append(nest) components_objs.append(nest)
@@ -76,6 +76,7 @@ classes = (
operator.UnlinkIfc, operator.UnlinkIfc,
operator.UnloadLink, operator.UnloadLink,
workspace.ExploreHotkey, workspace.ExploreHotkey,
operator.GenerateUVMap,
prop.LibraryBreadcrumb, prop.LibraryBreadcrumb,
prop.LibraryElement, prop.LibraryElement,
prop.FilterCategory, prop.FilterCategory,
+226 -106
View File
@@ -178,9 +178,18 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_description = ( bl_description = (
"Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file." "Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file."
) )
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(
append_all: bpy.props.BoolProperty(default=False) default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) ) # pyright: ignore[reportRedeclaration]
append_all: bpy.props.BoolProperty(default=False) # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path", default=False
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filter_glob: str
append_all: bool
use_relative_path: bool
reload_previous_file = False reload_previous_file = False
@@ -558,7 +567,11 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator):
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator): class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.append_library_element_by_query" bl_idname = "bim.append_library_element_by_query"
bl_label = "Append Library Element By Query" bl_label = "Append Library Element By Query"
query: bpy.props.StringProperty(name="Query")
query: bpy.props.StringProperty(name="Query") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
query: str
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -587,9 +600,16 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
"Append element to the current project.\n\n" "Append element to the current project.\n\n"
"ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)" "ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)"
) )
definition: bpy.props.IntProperty() definition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
prop_index: bpy.props.IntProperty() prop_index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"}) assume_unique_by_name: bpy.props.BoolProperty(
name="Assume Unique By Name", default=True, options={"SKIP_SAVE"}
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
definition: int
prop_index: int
assume_unique_by_name: bool
file: ifcopenshell.file file: ifcopenshell.file
@@ -618,8 +638,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if not element: if not element:
return {"FINISHED"} return {"FINISHED"}
if element.is_a("IfcTypeProduct"): if element.is_a("IfcTypeProduct"):
# Store opening template from library if it exists
self.store_opening_template_from_library(element, library_file)
self.import_type_from_ifc(element, context) self.import_type_from_ifc(element, context)
elif element.is_a("IfcProduct"): elif element.is_a("IfcProduct"):
# NOTE: Non-types are not exposed in UI directly # NOTE: Non-types are not exposed in UI directly
@@ -720,53 +738,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()): if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
ifc_importer.create_style(element) ifc_importer.create_style(element)
def store_opening_template_from_library(
self, element: ifcopenshell.entity_instance, library_file: ifcopenshell.file
) -> None:
"""
Find an opening representation in the library and copy it to the current file
as a template. Store the template ID on the type for later retrieval.
"""
try:
library_element = library_file.by_guid(element.GlobalId)
except:
return
# Find occurrences with openings in the library
library_occurrences = ifcopenshell.util.element.get_types(library_element)
for occurrence in library_occurrences:
if not getattr(occurrence, "FillsVoids", None):
continue
library_opening = occurrence.FillsVoids[0].RelatingOpeningElement
library_opening_rep = ifcopenshell.util.representation.get_representation(
library_opening, "Model", "Body", "MODEL_VIEW"
)
if not library_opening_rep:
continue
# Check if mapped representation
if (
library_opening_rep.RepresentationType == "MappedRepresentation"
and len(library_opening_rep.Items) == 1
and library_opening_rep.Items[0].is_a("IfcMappedItem")
):
mapped_rep = library_opening_rep.Items[0].MappingSource.MappedRepresentation
# Store ALL representation types (Tessellation, SweptSolid, etc.)
template_rep = ifcopenshell.util.element.copy_deep(
self.file, mapped_rep, exclude=["IfcGeometricRepresentationContext"]
)
# Store reference in type's Description
current_desc = element.Description or ""
element.Description = f"{current_desc}||BonsaiOpeningTemplate:{template_rep.id()}"
return
break
class EditProjectLibrary(bpy.types.Operator): class EditProjectLibrary(bpy.types.Operator):
bl_idname = "bim.edit_project_library" bl_idname = "bim.edit_project_library"
@@ -988,24 +959,28 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_label = "Load Project" bl_label = "Load Project"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Load an existing IFC project" bl_description = "Load an existing IFC project"
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"}) filepath: bpy.props.StringProperty(
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"}) subtype="FILE_PATH", options={"SKIP_SAVE"}
is_advanced: bpy.props.BoolProperty( ) # pyright: ignore[reportRedeclaration]
filter_glob: bpy.props.StringProperty(
default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
is_advanced: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Enable Advanced Mode", name="Enable Advanced Mode",
description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings", description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings",
default=False, default=False,
) )
use_relative_path: bpy.props.BoolProperty( use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Use Relative Path", name="Use Relative Path",
description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved", description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved",
default=False, default=False,
) )
should_start_fresh_session: bpy.props.BoolProperty( should_start_fresh_session: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Should Start Fresh Session", name="Should Start Fresh Session",
description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option", description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option",
default=True, default=True,
) )
import_without_ifc_data: bpy.props.BoolProperty( import_without_ifc_data: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Import Without IFC Data", name="Import Without IFC Data",
description=( description=(
"Import IFC objects as Blender objects without any IFC metadata and authoring capabilities." "Import IFC objects as Blender objects without any IFC metadata and authoring capabilities."
@@ -1013,9 +988,20 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
), ),
default=False, default=False,
) )
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) use_detailed_tooltip: bpy.props.BoolProperty(
default=False, options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
filename_ext = ".ifc" filename_ext = ".ifc"
if TYPE_CHECKING:
filepath: str
filter_glob: str
is_advanced: bool
use_relative_path: bool
should_start_fresh_session: bool
import_without_ifc_data: bool
use_detailed_tooltip: bool
@classmethod @classmethod
def description(cls, context, properties): def description(cls, context, properties):
tooltip = cls.bl_description tooltip = cls.bl_description
@@ -1116,7 +1102,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
else: else:
return self.finish_loading_project(context) return self.finish_loading_project(context)
def finish_loading_project(self, context): def finish_loading_project(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
try: try:
filepath = self.get_filepath() filepath = self.get_filepath()
if not self.is_existing_ifc_file(): if not self.is_existing_ifc_file():
@@ -1314,7 +1300,10 @@ class ToggleFilterCategories(bpy.types.Operator):
bl_idname = "bim.toggle_filter_categories" bl_idname = "bim.toggle_filter_categories"
bl_label = "Toggle Filter Categories" bl_label = "Toggle Filter Categories"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
should_select: bpy.props.BoolProperty(name="Should Select", default=True) should_select: bpy.props.BoolProperty(name="Should Select", default=True) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
should_select: bool
def execute(self, context): def execute(self, context):
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
@@ -1338,6 +1327,14 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
default=False, default=False,
) )
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
query: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
name="Query",
description=(
"Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n"
"Default query - IfcElement, but excluding IfcProxy, IfcSpatialStructureElement, IfcSpatialElement, IfcFeatureElement."
),
)
filename_ext = ".ifc" filename_ext = ".ifc"
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -1347,20 +1344,25 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
filter_glob: str filter_glob: str
use_relative_path: bool use_relative_path: bool
use_cache: bool use_cache: bool
query: str
def draw(self, context): def draw(self, context):
assert self.layout
pprops = tool.Project.get_project_props() pprops = tool.Project.get_project_props()
row = self.layout.row() row = self.layout.row()
row.prop(self, "use_relative_path") row.prop(self, "use_relative_path")
row = self.layout.row() row = self.layout.row()
row.prop(self, "use_cache") row.prop(self, "use_cache")
row = self.layout.row() row = self.layout.row()
row.prop(pprops, "false_origin_mode") row.label(text="False Origin Mode:")
row = self.layout.row()
row.prop(pprops, "false_origin_mode", text="")
if pprops.false_origin_mode == "MANUAL": if pprops.false_origin_mode == "MANUAL":
row = self.layout.row() row = self.layout.row()
row.prop(pprops, "false_origin") row.prop(pprops, "false_origin")
row = self.layout.row() row = self.layout.row()
row.prop(pprops, "project_north") row.prop(pprops, "project_north")
self.layout.prop(self, "query", placeholder="IfcElement")
def _execute(self, context): def _execute(self, context):
start = time.time() start = time.time()
@@ -1393,7 +1395,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
new.ifc_definition_id = reference.id() new.ifc_definition_id = reference.id()
new.name = filepath new.name = filepath
new.filepath = filepath new.filepath = filepath
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache) bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache, query=self.query)
class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator): class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
@@ -1401,7 +1403,11 @@ class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Unlink IFC" bl_label = "Unlink IFC"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Remove the selected file from the link list" bl_description = "Remove the selected file from the link list"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def _execute(self, context): def _execute(self, context):
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
@@ -1421,7 +1427,11 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Unload Link" bl_label = "Unload Link"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Unload the selected linked file" bl_description = "Unload the selected linked file"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def _execute(self, context): def _execute(self, context):
link = tool.Project.get_project_props().links[self.link_index] link = tool.Project.get_project_props().links[self.link_index]
@@ -1446,10 +1456,12 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration] use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration]
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING: if TYPE_CHECKING:
link_index: int link_index: int
use_cache: bool use_cache: bool
query: str
def _execute(self, context): def _execute(self, context):
self.link = tool.Project.get_project_props().links[self.link_index] self.link = tool.Project.get_project_props().links[self.link_index]
@@ -1491,8 +1503,20 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
def link_ifc(self) -> Union[set[str], None]: def link_ifc(self) -> Union[set[str], None]:
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend") blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5") h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
if not self.use_cache and blend_filepath.exists(): def should_clear_cache() -> bool:
if not self.use_cache:
return True
if not blend_filepath.exists():
return False
data = json.loads(json_filepath.read_text())
# Empty 'query' - model loaded without custom query.
# Missing 'query' - model was loaded before custom queries were introduced in Bonsai.
query = data.get("query", "")
return query != self.query
if should_clear_cache():
os.remove(blend_filepath) os.remove(blend_filepath)
if not blend_filepath.exists(): if not blend_filepath.exists():
@@ -1520,7 +1544,7 @@ def run():
pprops.project_north = "{pprops.project_north}" pprops.project_north = "{pprops.project_north}"
# Use absolute path to be safe from cwd changes. # Use absolute path to be safe from cwd changes.
try: try:
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}") bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}", query={repr(self.query)})
except RuntimeError as e: except RuntimeError as e:
# Operator failed (returned CANCELLED with error report) # Operator failed (returned CANCELLED with error report)
print(f"Failed to load linked project: {{e}}") print(f"Failed to load linked project: {{e}}")
@@ -1606,7 +1630,11 @@ class ReloadLink(bpy.types.Operator):
bl_label = "Reload Link" bl_label = "Reload Link"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Reload the selected file" bl_description = "Reload the selected file"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context): def execute(self, context):
bpy.ops.bim.unload_link(link_index=self.link_index) bpy.ops.bim.unload_link(link_index=self.link_index)
@@ -1618,7 +1646,11 @@ class ToggleLinkSelectability(bpy.types.Operator):
bl_label = "Toggle Link Selectability" bl_label = "Toggle Link Selectability"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle selectability" bl_description = "Toggle selectability"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context): def execute(self, context):
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
@@ -1788,7 +1820,11 @@ class SelectLinkHandle(bpy.types.Operator):
bl_label = "Select Link Handle" bl_label = "Select Link Handle"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Select link empty object handle" bl_description = "Select link empty object handle"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context): def execute(self, context):
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
@@ -1846,11 +1882,28 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc" filename_ext = ".ifc"
supported_filexts = (".ifc", ".ifczip", ".ifcjson") supported_filexts = (".ifc", ".ifczip", ".ifcjson")
filter_glob: bpy.props.StringProperty(default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version") default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False) ) # pyright: ignore[reportRedeclaration]
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"}) json_version: bpy.props.EnumProperty(
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version"
) # pyright: ignore[reportRedeclaration]
json_compact: bpy.props.BoolProperty(
name="Export Compact IFCJSON", default=False
) # pyright: ignore[reportRedeclaration]
should_save_as: bpy.props.BoolProperty(
name="Should Save As", default=False, options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path", default=False
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filter_glob: str
json_version: str
json_compact: bool
should_save_as: bool
use_relative_path: bool
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -2000,6 +2053,12 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file." bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""See ``bim.link_ifc``."""
if TYPE_CHECKING:
query: str
file: ifcopenshell.file file: ifcopenshell.file
meshes: dict[str, bpy.types.Mesh] meshes: dict[str, bpy.types.Mesh]
# Material names is derived from diffuse as in 'r-g-b-a'. # Material names is derived from diffuse as in 'r-g-b-a'.
@@ -2049,14 +2108,17 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
tool.Loader.settings.context_settings = tool.Loader.create_settings() tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
self.elements = set(self.file.by_type("IfcElement")) if self.query:
if self.file.schema in ("IFC2X3", "IFC4"): self.elements = ifcopenshell.util.selector.filter_elements(self.file, self.query)
self.elements |= set(self.file.by_type("IfcProxy"))
if self.file.schema == "IFC2X3":
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
else: else:
self.elements |= set(self.file.by_type("IfcSpatialElement")) self.elements = set(self.file.by_type("IfcElement"))
self.elements -= set(self.file.by_type("IfcFeatureElement")) if self.file.schema in ("IFC2X3", "IFC4"):
self.elements |= set(self.file.by_type("IfcProxy"))
if self.file.schema == "IFC2X3":
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
else:
self.elements |= set(self.file.by_type("IfcSpatialElement"))
self.elements -= set(self.file.by_type("IfcFeatureElement"))
if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin: if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin:
tool.Loader.set_manual_blender_offset(self.file) tool.Loader.set_manual_blender_offset(self.file)
@@ -2081,6 +2143,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
"false_origin_mode": pprops.false_origin_mode, "false_origin_mode": pprops.false_origin_mode,
"false_origin": pprops.false_origin, "false_origin": pprops.false_origin,
"project_north": pprops.project_north, "project_north": pprops.project_north,
"query": self.query,
} }
with open(self.json_filepath, "w") as f: with open(self.json_filepath, "w") as f:
json.dump(data, f) json.dump(data, f)
@@ -2142,6 +2205,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
if iterator.initialize(): if iterator.initialize():
while True: # Main loop. while True: # Main loop.
shape = iterator.get() shape = iterator.get()
assert isinstance(shape, W.TriangulationElement)
results.add(self.file.by_id(shape.id)) results.add(self.file.by_id(shape.id))
geometry = shape.geometry geometry = shape.geometry
@@ -2372,18 +2436,23 @@ class HideQueriedLinkedElement(bpy.types.Operator):
bl_label = "Hide Queried Linked Element" bl_label = "Hide Queried Linked Element"
bl_description = ( bl_description = (
"Hide geometry for currently queried linked element.\n\n" "Hide geometry for currently queried linked element.\n\n"
"ALT+Click (or ALT+H in Explore Tool) to unhide all geometry for currently selected linked model.\n" "SHIFT+Click (or SHIFT+H in Explore Tool) to hide everything "
"(Not Yet Implemented) SHIFT+Click to hide everything but currently queried element." "in the currently selected model, but the queried element.\n"
"ALT+Click (or ALT+H in Explore Tool) to unhide all geometry for currently selected linked model.\n\n"
"Known limitation: doesn't work with UNDO."
) )
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING: if TYPE_CHECKING:
unhide_all: bool unhide_all: bool
hide_all_except: bool
def invoke(self, context, event): def invoke(self, context, event):
self.unhide_all = event.alt self.unhide_all = event.alt
self.hide_all_except = event.shift
return self.execute(context) return self.execute(context)
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
@@ -2392,6 +2461,9 @@ class HideQueriedLinkedElement(bpy.types.Operator):
if self.unhide_all: if self.unhide_all:
return self.run_unhide_all() return self.run_unhide_all()
if self.hide_all_except:
return self.run_hide_all_except()
obj = props.queried_obj obj = props.queried_obj
if not obj: if not obj:
self.report({"INFO"}, "No object is queried to hide.") self.report({"INFO"}, "No object is queried to hide.")
@@ -2413,6 +2485,21 @@ class HideQueriedLinkedElement(bpy.types.Operator):
self.report({"INFO"}, "All linked model geometry is unhidden.") self.report({"INFO"}, "All linked model geometry is unhidden.")
return {"FINISHED"} return {"FINISHED"}
def run_hide_all_except(self) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Project.get_project_props()
obj = props.queried_obj
if not obj:
self.report({"INFO"}, "No object is queried.")
return {"FINISHED"}
link = props.active_link
if not link:
self.report({"INFO"}, "No linked model is currently selected.")
return {"FINISHED"}
guid = props.queried_guid
tool.Project.Link.hide_all_elements_except(link, obj, guid)
self.report({"INFO"}, "All other linked model geometry is now hidden.")
return {"FINISHED"}
class AppendInspectedLinkedElement(AppendLibraryElement): class AppendInspectedLinkedElement(AppendLibraryElement):
bl_idname = "bim.append_inspected_linked_element" bl_idname = "bim.append_inspected_linked_element"
@@ -2471,7 +2558,7 @@ class EnableCulling(bpy.types.Operator):
self.total_mousemoves = 0 self.total_mousemoves = 0
self.cullable_objects = [] self.cullable_objects = []
def modal(self, context, event): def modal(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
if not LinksData.enable_culling: if not LinksData.enable_culling:
for obj in bpy.context.visible_objects: for obj in bpy.context.visible_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc"): if obj.type == "MESH" and obj.name.startswith("Ifc"):
@@ -2502,7 +2589,7 @@ class EnableCulling(bpy.types.Operator):
return {"PASS_THROUGH"} return {"PASS_THROUGH"}
def is_view_changed(self, context): def is_view_changed(self, context: bpy.types.Context) -> bool:
view_matrix = context.region_data.view_matrix view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix projection_matrix = context.region_data.window_matrix
vp_matrix = projection_matrix @ view_matrix vp_matrix = projection_matrix @ view_matrix
@@ -2517,7 +2604,7 @@ class EnableCulling(bpy.types.Operator):
return True return True
return False return False
def is_object_in_view(self, obj, context, camera_position): def is_object_in_view(self, obj: bpy.types.Object, context: bpy.types.Context, camera_position: Vector) -> bool:
# Get the view matrix and the projection matrix from the active viewport # Get the view matrix and the projection matrix from the active viewport
view_matrix = context.region_data.view_matrix view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix projection_matrix = context.region_data.window_matrix
@@ -2544,7 +2631,7 @@ class EnableCulling(bpy.types.Operator):
return False return False
return True return True
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set["rna_enums.OperatorReturnItems"]:
LinksData.enable_culling = True LinksData.enable_culling = True
self.cullable_objects = [] self.cullable_objects = []
for obj in bpy.context.visible_objects: for obj in bpy.context.visible_objects:
@@ -2698,10 +2785,7 @@ class CreateClippingPlane(bpy.types.Operator):
self.report({"INFO"}, "Maximum of six clipping planes allowed.") self.report({"INFO"}, "Maximum of six clipping planes allowed.")
return {"FINISHED"} return {"FINISHED"}
assert context.screen tool.Blender.update_all_viewports(context)
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
assert context.region and context.region_data assert context.region and context.region_data
region = context.region region = context.region
@@ -2834,8 +2918,16 @@ class IFCFileHandlerOperator(bpy.types.Operator):
bl_label = "Import .ifc file" bl_label = "Import .ifc file"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"}) directory: bpy.props.StringProperty(
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"}) subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"}
) # pyright: ignore[reportRedeclaration]
files: bpy.props.CollectionProperty(
type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"}
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
directory: str
files: list[bpy.types.OperatorFileListElement]
def invoke(self, context, event): def invoke(self, context, event):
# Keeping code in .invoke() as we'll probably add some # Keeping code in .invoke() as we'll probably add some
@@ -2886,7 +2978,10 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Tool" bl_label = "Measure Tool"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty() measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
measure_type: str
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -2982,7 +3077,10 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Face Area Tool" bl_label = "Measure Face Area Tool"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty() measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
measure_type: str
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -3086,7 +3184,10 @@ class ClearMeasurement(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
polyline_props = tool.Model.get_polyline_props() polyline_props = tool.Model.get_polyline_props()
return len(polyline_props.measurement_polyline) > 0 if len(polyline_props.measurement_polyline) > 0:
return True
cls.poll_message_set("No measurement to clear.")
return False
def execute(self, context): def execute(self, context):
polyline_props = tool.Model.get_polyline_props() polyline_props = tool.Model.get_polyline_props()
@@ -3190,7 +3291,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
super().invoke(context, event) super().invoke(context, event)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
def cancel_tool(self, context): def cancel_tool(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
context.workspace.status_text_set(text=None) context.workspace.status_text_set(text=None)
if hasattr(self, "tool_state"): if hasattr(self, "tool_state"):
self.tool_state.plane_method = None self.tool_state.plane_method = None
@@ -3198,7 +3299,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
tool.Blender.update_viewport() tool.Blender.update_viewport()
return {"CANCELLED"} return {"CANCELLED"}
def handle_custom_instructions(self, context): def handle_custom_instructions(self, context: bpy.types.Context) -> None:
if len(self.selected_points) == 0: if len(self.selected_points) == 0:
instruction_text = "Click First Point on Image" instruction_text = "Click First Point on Image"
elif len(self.selected_points) == 1: elif len(self.selected_points) == 1:
@@ -3213,14 +3314,14 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
context.workspace.status_text_set(text=instruction_text) context.workspace.status_text_set(text=instruction_text)
def calculate_distance(self): def calculate_distance(self) -> None:
if len(self.selected_points) == 2: if len(self.selected_points) == 2:
point1 = self.selected_points[0] point1 = self.selected_points[0]
point2 = self.selected_points[1] point2 = self.selected_points[1]
distance_3d = (point2 - point1).length distance_3d = (point2 - point1).length
self.calculated_distance = distance_3d / self.unit_scale self.calculated_distance = distance_3d / self.unit_scale
def apply_scaling(self, context): def apply_scaling(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
if len(self.selected_points) != 2: if len(self.selected_points) != 2:
self.report({"ERROR"}, "Two points must be selected") self.report({"ERROR"}, "Two points must be selected")
return {"CANCELLED"} return {"CANCELLED"}
@@ -3278,7 +3379,10 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
bl_idname = "bim.load_blend_metadata_and_ifc" bl_idname = "bim.load_blend_metadata_and_ifc"
bl_label = "Load Blend Metadata and IFC" bl_label = "Load Blend Metadata and IFC"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(name="IFC File Path", default="") filepath: bpy.props.StringProperty(name="IFC File Path", default="") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filepath: str
def execute(self, context): def execute(self, context):
ifc_file = self.filepath ifc_file = self.filepath
@@ -3312,3 +3416,19 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
bpy.app.handlers.load_post.append(load_handler) bpy.app.handlers.load_post.append(load_handler)
bpy.ops.wm.open_mainfile(filepath=metadata_path) bpy.ops.wm.open_mainfile(filepath=metadata_path)
return {"FINISHED"} return {"FINISHED"}
class GenerateUVMap(bpy.types.Operator):
bl_idname = "bim.generate_uv_map"
bl_label = "Generate UV Map"
bl_description = "Generate UV map for selected mesh."
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
if not obj or not isinstance(obj.data, bpy.types.Mesh):
self.report({"ERROR"}, "No valid mesh selected.")
return {"CANCELLED"}
tool.Loader.load_generated_uv_map(obj.data)
self.report({"INFO"}, "Generated UV map for selected mesh.")
return {"FINISHED"}
+4 -4
View File
@@ -230,11 +230,11 @@ class Link(PropertyGroup):
) )
georeferenced: EnumProperty( georeferenced: EnumProperty(
name="Georeferenced", name="Georeferenced",
description="Georeferencing status: compatibility between host and linked model", description="Georeferencing status, compatibility between host and linked model",
items=[ items=[
("NONE", "No Georef", "Linked model has no georeferencing"), ("NONE", "No Georef", "Linked model has no georeferencing", "QUESTION", 0),
("NOT_COMPATIBLE", "Not Compatible", "Has geo data but CRS differ from host"), ("NOT_COMPATIBLE", "Not Compatible", "Has geo data but CRS differ from host", "ERROR", 1),
("FULL_COMPATIBLE", "Full Compatible", "Both CRS name and vertical datum match host"), ("FULL_COMPATIBLE", "Full Compatible", "Both CRS name and vertical datum match host", "WORLD", 2),
], ],
default="NONE", default="NONE",
) )
+10 -8
View File
@@ -255,7 +255,7 @@ class BIM_PT_project(Panel):
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.operator("bim.load_project_elements") row.operator("bim.load_project_elements")
def draw_editing_buttons(self, context, row): def draw_editing_buttons(self, context: object, row: bpy.types.UILayout) -> None:
pprops = self.props pprops = self.props
if tool.Ifc.get(): if tool.Ifc.get():
if pprops.is_editing: if pprops.is_editing:
@@ -496,7 +496,7 @@ class BIM_PT_links(Panel):
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
else: else:
row.operator("bim.load_link", text="", icon="LINKED").link_index = index row.operator("bim.load_link", text="", icon="LINKED").link_index = index
row.operator("bim.unlink_ifc", text="", icon="X").link_index = index row.operator("bim.unlink_ifc", text="", icon="X").link_index = index
self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index") self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index")
if LinksData.enable_culling: if LinksData.enable_culling:
@@ -619,12 +619,14 @@ class BIM_UL_links(UIList):
): ):
row = layout.row(align=True) row = layout.row(align=True)
if item.is_loaded: if item.is_loaded:
if item.georeferenced == "NONE": from bonsai.bim.module.project.prop import Link
row.label(text="", icon="QUESTION")
elif item.georeferenced == "NOT_COMPATIBLE": s = Link.bl_rna
row.label(text="", icon="ERROR") geo_prop = s.properties["georeferenced"]
elif item.georeferenced == "FULL_COMPATIBLE": assert isinstance(geo_prop, bpy.types.EnumProperty)
row.label(text="", icon="WORLD") enum_item = geo_prop.enum_items[item.georeferenced]
op = row.operator("bim.show_description", text="", icon=enum_item.icon, emboss=False)
op.description = f"{geo_prop.description}\n{enum_item.name}: {enum_item.description}"
if item.has_transformation: if item.has_transformation:
row.label(text="", icon="OBJECT_ORIGIN") row.label(text="", icon="OBJECT_ORIGIN")
@@ -41,6 +41,7 @@ class ExploreTool(bpy.types.WorkSpaceTool):
("bim.explore_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}), ("bim.explore_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
("bim.explore_hotkey", {"type": "S", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_S")]}), ("bim.explore_hotkey", {"type": "S", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_S")]}),
("bim.explore_hotkey", {"type": "H", "value": "PRESS"}, {"properties": [("hotkey", "H")]}), ("bim.explore_hotkey", {"type": "H", "value": "PRESS"}, {"properties": [("hotkey", "H")]}),
("bim.explore_hotkey", {"type": "H", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_H")]}),
("bim.explore_hotkey", {"type": "H", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_H")]}), ("bim.explore_hotkey", {"type": "H", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_H")]}),
) )
@@ -70,21 +71,26 @@ class ExploreTool(bpy.types.WorkSpaceTool):
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_M") row.label(text="", icon="EVENT_M")
row = layout.row(align=True)
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT") op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
op.hotkey = "S_M" op.hotkey = "S_M"
row = layout.row(align=True) row = layout.row(align=True)
row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=True) row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=True)
row = layout.row(align=True)
op = row.operator("bim.clear_measurement", text="", icon="X") op = row.operator("bim.clear_measurement", text="", icon="X")
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_S") row.label(text="", icon="EVENT_S")
row = layout.row(align=True)
op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE") op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE")
op.hotkey = "S_S" op.hotkey = "S_S"
op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them" op.description = (
"Scale Image Annotation.\n\n"
"Allows to scale an IfcReferenceImage.\n\n"
"Select image, select tool. "
"Check lower left corner instructions to select two points and provide real distance between them"
)
row = layout.row(align=True)
row.operator("bim.generate_uv_map", icon="UV")
class ExploreHotkey(bpy.types.Operator): class ExploreHotkey(bpy.types.Operator):
@@ -147,5 +153,8 @@ class ExploreHotkey(bpy.types.Operator):
def hotkey_H(self) -> None: def hotkey_H(self) -> None:
bpy.ops.bim.hide_queried_linked_element() bpy.ops.bim.hide_queried_linked_element()
def hotkey_S_H(self) -> None:
bpy.ops.bim.hide_queried_linked_element(hide_all_except=True)
def hotkey_A_H(self) -> None: def hotkey_A_H(self) -> None:
bpy.ops.bim.hide_queried_linked_element(unhide_all=True) bpy.ops.bim.hide_queried_linked_element(unhide_all=True)
+1 -1
View File
@@ -262,7 +262,7 @@ class BIM_PT_object_psets(Panel):
row = self.layout.row(align=True) row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="") prop_with_search(row, props, "pset_name", text="")
if props.pset_name != "BBIM_BSDD" and not props.pset_name.startswith(tool.Bsdd.identifier_url): if props.pset_name != "BBIM_BSDD" and not props.pset_name.startswith(tool.Bsdd.identifier_url()):
op = row.operator("bim.add_pset", icon="ADD", text="") op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj = obj.name op.obj = obj.name
op.obj_type = "Object" op.obj_type = "Object"
@@ -321,7 +321,7 @@ def get_gross_perimeter(o: bpy.types.Object) -> float:
return gross_perimeter return gross_perimeter
def get_space_net_perimeter(obj: bpy.types.Object) -> float: def get_space_net_perimeter(obj: bpy.types.Object) -> None:
pass pass
@@ -188,7 +188,7 @@ class BIMResourceProperties(PropertyGroup):
@property @property
def productivity(self) -> "BIMResourceProductivity": def productivity(self) -> "BIMResourceProductivity":
assert bpy.context.scene assert bpy.context.scene
productivity = bpy.context.scene.BIMResourceProductivity productivity = bpy.context.scene.BIMResourceProductivity # pyright: ignore[reportAttributeAccessIssue]
assert isinstance(productivity, BIMResourceProductivity) assert isinstance(productivity, BIMResourceProductivity)
return productivity return productivity
@@ -41,6 +41,7 @@ classes = (
operator.SelectByProperty, operator.SelectByProperty,
operator.SelectFilterElements, operator.SelectFilterElements,
operator.SelectGlobalId, operator.SelectGlobalId,
operator.SelectQueryElements,
operator.SelectIfcClass, operator.SelectIfcClass,
operator.SelectSimilar, operator.SelectSimilar,
operator.ShowAllElements, operator.ShowAllElements,
@@ -42,6 +42,8 @@ from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty from bonsai.bim.prop import StrProperty
if TYPE_CHECKING: if TYPE_CHECKING:
from bpy.stub_internal import rna_enums
from bonsai.bim.prop import BIMFacet from bonsai.bim.prop import BIMFacet
@@ -617,7 +619,7 @@ class SelectFilterElements(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class ApplyFilterFromText(Operator, tool.Ifc.Operator): class ApplyFilterFromText(Operator):
bl_idname = "bim.apply_filter_from_text" bl_idname = "bim.apply_filter_from_text"
bl_label = "Apply Filter Configuration" bl_label = "Apply Filter Configuration"
bl_description = "Apply the JSON filter configuration from the current text block" bl_description = "Apply the JSON filter configuration from the current text block"
@@ -791,6 +793,27 @@ class Search(Operator):
return {"FINISHED"} return {"FINISHED"}
class SelectQueryElements(Operator):
bl_idname = "bim.select_query_elements"
bl_label = "Select Query Elements"
bl_description = "Select elements matching an provided selector query"
bl_options = {"REGISTER", "UNDO"}
query: StringProperty(name="Query") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
query: str
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), self.query)
objs = [obj for e in results if isinstance(obj := tool.Ifc.get_object(e), bpy.types.Object)]
active_object = context.active_object or next(iter(objs), None)
selection = tool.Blender.validate_object_selection(context, active_object, objs)
tool.Blender.set_objects_selection(*selection, clear_previous_selection=False)
self.report({"INFO"}, f"{len(results)} Results, {len(selection.selected_objects)} Objects Selected")
return {"FINISHED"}
class SaveSearch(Operator, tool.Ifc.Operator): class SaveSearch(Operator, tool.Ifc.Operator):
bl_idname = "bim.save_search" bl_idname = "bim.save_search"
bl_label = "Save Search" bl_label = "Save Search"
@@ -1053,8 +1076,8 @@ class ColourByProperty(Operator):
colourscheme[str(values[index])]["total"] += 1 colourscheme[str(values[index])]["total"] += 1
obj.color = (*tool.Search.get_quantitative_palette(palette, value, min_value, max_value), 1) obj.color = (*tool.Search.get_quantitative_palette(palette, value, min_value, max_value), 1)
if areas := [a for a in context.screen.areas if a.type == "VIEW_3D"]: assert (space := tool.Blender.get_view3d_space())
areas[0].spaces[0].shading.color_type = "OBJECT" space.shading.color_type = "OBJECT"
props.colourscheme.clear() props.colourscheme.clear()
@@ -1078,16 +1101,18 @@ class ColourByProperty(Operator):
return (1, value) return (1, value)
def store_state(self, context): def store_state(self, context):
if areas := [a for a in context.screen.areas if a.type == "VIEW_3D"]: if space := tool.Blender.get_view3d_space():
self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type} self.transaction_data = {"color_type": space.shading.color_type}
def rollback(self, data): def rollback(self, data):
if data: if data:
data["area"].spaces[0].shading.color_type = data["color_type"] assert (space := tool.Blender.get_view3d_space())
space.shading.color_type = data["color_type"]
def commit(self, data): def commit(self, data):
if data: if data:
data["area"].spaces[0].shading.color_type = "OBJECT" assert (space := tool.Blender.get_view3d_space())
space.shading.color_type = "OBJECT"
class SelectByProperty(Operator): class SelectByProperty(Operator):
@@ -1415,7 +1440,7 @@ class ShowAllElements(Operator):
return {"FINISHED"} return {"FINISHED"}
class SelectSimilar(Operator, tool.Ifc.Operator): class SelectSimilar(Operator):
bl_idname = "bim.select_similar" bl_idname = "bim.select_similar"
bl_label = "Select Similar" bl_label = "Select Similar"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
+28 -12
View File
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Optional
import bpy import bpy
@@ -37,8 +39,11 @@ from bonsai.bim.module.sequence.data import (
if TYPE_CHECKING: if TYPE_CHECKING:
from bonsai.bim.module.sequence.prop import ( from bonsai.bim.module.sequence.prop import (
BIMTaskTreeProperties, BIMTaskTreeProperties,
BIMTaskTypeColor,
BIMWorkScheduleProperties, BIMWorkScheduleProperties,
Task, Task,
TaskProduct,
TaskResource,
) )
from bonsai.bim.prop import Attribute from bonsai.bim.prop import Attribute
@@ -799,23 +804,24 @@ class BIM_UL_task_columns(UIList):
self, self,
context, context,
layout: bpy.types.UILayout, layout: bpy.types.UILayout,
data: "BIMWorkScheduleProperties", data: BIMWorkScheduleProperties,
item: "Attribute", item: Attribute,
icon, icon,
active_data, active_data,
active_propname, active_propname,
): ):
props = tool.Sequence.get_work_schedule_props()
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
row.prop(item, "name", emboss=False, text="") row.prop(item, "name", emboss=False, text="")
if props.sort_column == item.name: if data.sort_column == item.name:
row.label(text="", icon="SORTALPHA") row.label(text="", icon="SORTALPHA")
row.operator("bim.remove_task_column", text="", icon="X").name = item.name row.operator("bim.remove_task_column", text="", icon="X").name = item.name
class BIM_UL_task_inputs(UIList): class BIM_UL_task_inputs(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self, context, layout: bpy.types.UILayout, data, item: TaskProduct, icon, active_data, active_propname
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF") op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF")
@@ -825,7 +831,9 @@ class BIM_UL_task_inputs(UIList):
class BIM_UL_task_resources(UIList): class BIM_UL_task_resources(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self, context, layout: bpy.types.UILayout, data, item: TaskResource, icon, active_data, active_propname
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
row.operator("bim.go_to_resource", text="", icon="STYLUS_PRESSURE").resource = item.ifc_definition_id row.operator("bim.go_to_resource", text="", icon="STYLUS_PRESSURE").resource = item.ifc_definition_id
@@ -834,7 +842,9 @@ class BIM_UL_task_resources(UIList):
class BIM_UL_animation_colors(UIList): class BIM_UL_animation_colors(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self, context, layout: bpy.types.UILayout, data, item: BIMTaskTypeColor, icon, active_data, active_propname
) -> None:
if item: if item:
row = layout.row() row = layout.row()
row.prop(item, "color", text="") row.prop(item, "color", text="")
@@ -842,7 +852,9 @@ class BIM_UL_animation_colors(UIList):
class BIM_UL_task_outputs(UIList): class BIM_UL_task_outputs(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self, context, layout: bpy.types.UILayout, data, item: TaskProduct, icon, active_data, active_propname
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF") op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF")
@@ -851,7 +863,9 @@ class BIM_UL_task_outputs(UIList):
class BIM_UL_product_input_tasks(UIList): class BIM_UL_product_input_tasks(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self, context, layout: bpy.types.UILayout, data, item: TaskProduct, icon, active_data, active_propname
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
op = row.operator("bim.go_to_task", text="", icon="STYLUS_PRESSURE") op = row.operator("bim.go_to_task", text="", icon="STYLUS_PRESSURE")
@@ -861,7 +875,9 @@ class BIM_UL_product_input_tasks(UIList):
class BIM_UL_product_output_tasks(UIList): class BIM_UL_product_output_tasks(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self, context, layout: bpy.types.UILayout, data, item: TaskProduct, icon, active_data, active_propname
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
op = row.operator("bim.go_to_task", text="", icon="STYLUS_PRESSURE") op = row.operator("bim.go_to_task", text="", icon="STYLUS_PRESSURE")
@@ -886,8 +902,8 @@ class BIM_UL_tasks(UIList):
self, self,
context, context,
layout: bpy.types.UILayout, layout: bpy.types.UILayout,
data: "BIMTaskTreeProperties", data: BIMTaskTreeProperties,
item: "Task", item: Task,
icon, icon,
active_data, active_data,
active_propname, active_propname,
@@ -512,7 +512,7 @@ class SetContainerVisibility(bpy.types.Operator):
containers -= set(tool.Ifc.get().by_type("IfcSpatialZone")) containers -= set(tool.Ifc.get().by_type("IfcSpatialZone"))
for container in containers: for container in containers:
if obj := tool.Ifc.get_object(container): if obj := tool.Ifc.get_object(container):
if collection := obj.BIMObjectProperties.collection: if collection := tool.Blender.get_object_bim_props(obj).collection:
collection.hide_viewport = True collection.hide_viewport = True
should_hide = False should_hide = False
else: else:
@@ -523,7 +523,7 @@ class SetContainerVisibility(bpy.types.Operator):
while queue: while queue:
container = queue.pop() container = queue.pop()
if obj := tool.Ifc.get_object(container): if obj := tool.Ifc.get_object(container):
if collection := obj.BIMObjectProperties.collection: if collection := tool.Blender.get_object_bim_props(obj).collection:
collection.hide_viewport = should_hide collection.hide_viewport = should_hide
if self.should_include_children: if self.should_include_children:
queue.extend(ifcopenshell.util.element.get_parts(container)) queue.extend(ifcopenshell.util.element.get_parts(container))
+2 -2
View File
@@ -251,7 +251,7 @@ class BIM_PT_grids(Panel):
bl_options = {"HEADER_LAYOUT_EXPAND"} bl_options = {"HEADER_LAYOUT_EXPAND"}
def draw(self, context): def draw(self, context):
self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids") self.layout.row().operator("bim.add_grid", icon="ADD", text="Add Grids")
def draw_header(self, context): def draw_header(self, context):
props = tool.Spatial.get_grid_props() props = tool.Spatial.get_grid_props()
@@ -357,7 +357,7 @@ class BIM_UL_elements(UIList):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.use_filter_show = True self.use_filter_show = True
def draw_toggle(self, row: bpy.types.UILayout, is_expanded: bool, index: int): def draw_toggle(self, row: bpy.types.UILayout, is_expanded: bool, index: int) -> None:
icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT" icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT"
row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index

Some files were not shown because too many files have changed in this diff Show More