Compare commits

..

2 Commits

Author SHA1 Message Date
Ryan Schultz cc90340b3b tweaked active operator 2024-10-26 14:22:18 -05:00
Ryan Schultz db075c7160 display the sum of all selected objects 2024-10-26 13:26:11 -05:00
1140 changed files with 24304 additions and 62919 deletions
-30
View File
@@ -1,30 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Error Description and Steps to Reproduce**
<!--
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
-->
**Attachments**
<!--
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
-->
**Debug information**
<!--
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
-->
-15
View File
@@ -1,15 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Feature Description**
<!--
Describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
-->
-21
View File
@@ -1,21 +0,0 @@
name: Dispatch Build IfcOpenShell
on:
workflow_dispatch:
jobs:
trigger-workflows:
runs-on: ubuntu-latest
strategy:
matrix:
workflow:
- 'Build IfcOpenShell Linux'
- 'Build IfcOpenShell Linux ARM'
- 'Build IfcOpenShell OSX'
- 'Build IfcOpenShell WASM / Pyodide'
- 'Build IfcOpenShell Windows'
steps:
- name: Trigger binary build workflows
uses: benc-uk/workflow-dispatch@v1
with:
workflow: ${{ matrix.workflow }}
-136
View File
@@ -1,136 +0,0 @@
name: Build IfcOpenShell OSX
on:
workflow_dispatch:
jobs:
build_ifcopenshell:
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- os: macos
runner: macos-13
arch: x64
oldarch:
- os: macos
runner: macos-14
arch: arm64
oldarch: m1
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
ref: ${{ matrix.os }}-${{ matrix.arch }}
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install Dependencies
run: |
brew update
brew install git bison autoconf automake libffi cmake findutils
echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -name install)
find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \;
- name: ccache
uses: hendrikmuhs/ccache-action@v1
with:
key: ${GITHUB_WORKFLOW}-${{ matrix.os }}
- name: Run Build Script
run: |
if [ "${{ matrix.os }}" == "macos" ]; then
DARWIN_C_SOURCE=-D_DARWIN_C_SOURCE
fi
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
- name: Pack Dependencies
run: |
cd build
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
done
- name: Commit and Push Changes to Build Repository
run: |
cd build
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add "$(find . -maxdepth 4 -name install)/*.tar.gz"
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || true
- name: Package .zip archives
run: |
VERSION=v`cat VERSION`
cd ./build/`uname`/*/10.9/install/ifcopenshell
mkdir ~/output
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell/*
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip $exe
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Overwrite aws cli
run: |
# Error: The `brew link` step did not complete successfully
# The formula built, but is not symlinked into /usr/local
# Could not symlink bin/idle3
# Target /usr/local/bin/idle3
# already exists. You may want to remove it:
# rm '/usr/local/bin/idle3'
#
# To force the link and overwrite all conflicting files:
# brew link --overwrite python@3.13
# https://github.com/rust-lang/rustup/pull/3989/files
brew install --overwrite awscli | true
- name: Upload .zip archives to S3
run: |
aws s3 cp ~/output s3://ifcopenshell-builds/ --recursive
-54
View File
@@ -1,54 +0,0 @@
name: Build IfcOpenShell WASM / Pyodide
on:
workflow_dispatch:
jobs:
build_ifcopenshell:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: recursive
path: IfcOpenShell
- name: Checkout Pyodide
uses: actions/checkout@v3
with:
submodules: recursive
repository: pyodide/pyodide
ref: '0.26.4'
token: ${{ secrets.BUILD_REPO_TOKEN }}
path: pyodide
- name: Build
run: |
VERSION=`cat IfcOpenShell/VERSION`
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/meta.yaml
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/setup.py
echo '#!/usr/bin/bash' > script.sh
echo 'cd pyodide' > script.sh
echo 'make && pip install ./pyodide-build' >> script.sh
echo 'cd ..' >> script.sh
echo 'mkdir -p packages/ifcopenshell' >> script.sh
echo 'cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell' >> script.sh
echo 'PYODIDE_ROOT=/src/pyodide \' >> script.sh
echo 'PATH=/src/pyodide/emsdk/emsdk:/src/pyodide/emsdk/emsdk/node/20.18.0_64bit/bin:/src/pyodide/emsdk/emsdk/upstream/emscripten:$PATH \' >> script.sh
echo 'pyodide build-recipes ifcopenshell --install' >> script.sh
chmod +x script.sh
sed -i s/--tty// pyodide/run_docker
pyodide/run_docker ./script.sh
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp312-cp312-emscripten_3_1_58_wasm32.whl
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload .zip archives to S3
run: |
aws s3 cp dist s3://ifcopenshell-builds/ --recursive --exclude "*" --include "*.whl"
-116
View File
@@ -1,116 +0,0 @@
name: Build IfcOpenShell Linux
on:
workflow_dispatch:
jobs:
build_ifcopenshell:
runs-on: ubuntu-22.04
container: rockylinux:8
steps:
- name: Install Dependencies
run: |
yum update -y
yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils
git config --global --add safe.directory '*'
- name: Install aws cli
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
./aws/install
rm -rf awscliv2.zip aws
aws --version
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
ref: rockylinux8-x64
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -name install)
find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \;
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ${GITHUB_WORKFLOW}-rockylinux8-x64
- name: Run Build Script
run: |
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
- name: Pack Dependencies
run: |
cd build
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
done
- name: Commit and Push Changes to Build Repository
run: |
cd build
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add "$(find . -maxdepth 4 -name install)/*.tar.gz"
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || true
- name: Package .zip archives
run: |
VERSION=v`cat VERSION`
cd ./build/`uname`/*/install/ifcopenshell
mkdir ~/output
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/*
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload .zip archives to S3
run: |
aws s3 cp ~/output s3://ifcopenshell-builds/ --recursive
-116
View File
@@ -1,116 +0,0 @@
name: Build IfcOpenShell Linux ARM
on:
workflow_dispatch:
jobs:
build_ifcopenshell:
runs-on: ubuntu-22.04-arm
container: arm64v8/rockylinux:8
steps:
- name: Install Dependencies
run: |
yum update -y
yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils
git config --global --add safe.directory '*'
- name: Install aws cli
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
./aws/install
rm -rf awscliv2.zip aws
aws --version
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
ref: rockylinux8-arm64
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -name install)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ${GITHUB_WORKFLOW}-rockylinux8-x64
- name: Run Build Script
run: |
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
- name: Pack Dependencies
run: |
cd build
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
done
- name: Commit and Push Changes to Build Repository
run: |
cd build
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add "$(find . -maxdepth 4 -name install)/*.tar.gz"
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || true
- name: Package .zip archives
run: |
VERSION=v`cat VERSION`
cd ./build/`uname`/*/install/ifcopenshell
mkdir ~/output
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell/*
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip $exe
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload .zip archives to S3
run: |
aws s3 cp ~/output s3://ifcopenshell-builds/ --recursive
-122
View File
@@ -1,122 +0,0 @@
name: Build IfcOpenShell Windows
on:
workflow_dispatch:
jobs:
build_ifcopenshell:
runs-on: windows-2019
strategy:
fail-fast: false
matrix:
python: ['3.9.11', '3.10.3', '3.11.8', '3.12.1', '3.13.0']
arch: ['x64']
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: _deps-vs2019-x64-installed
ref: windows-${{ matrix.arch }}
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Install Dependencies
run: |
choco install -y sed 7zip.install awscli
- name: Install Python
run: |
$installer = "python-${{ matrix.python }}-amd64.exe"
$url = "https://www.python.org/ftp/python/${{ matrix.python }}/$installer"
Invoke-WebRequest -Uri $url -OutFile $installer
Start-Process -Wait -FilePath .\$installer -ArgumentList '/quiet InstallAllUsers=0 PrependPath=0 Include_test=0 TargetDir=C:\Python\${{ matrix.python }}'
Remove-Item .\$installer
- name: Unpack Dependencies
run: |
cd _deps-vs2019-x64-installed
Get-ChildItem -Path . -Filter 'cache-*.zip' | ForEach-Object {
7z x $_.FullName
}
- name: Run Build Script
shell: cmd
run: |
setlocal EnableDelayedExpansion
SET PYTHON_VERSION=${{ matrix.python }}
for /f "tokens=1,2,3 delims=." %%a in ("%PYTHON_VERSION%") do (
set PY_VER_MAJOR_MINOR=%%a%%b
)
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
SET IFCOS_INSTALL_PYTHON=FALSE
cd win
echo y | call build-deps.cmd vs2019-x64 Release
SET PYTHONHOME=C:\Python\${{ matrix.python }}
call run-cmake.bat vs2019-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
call install-ifcopenshell.bat vs2019-x64 Release
- name: Pack Dependencies
run: |
cd _deps-vs2019-x64-installed
Get-ChildItem -Path . -Directory | ForEach-Object {
$cacheFile = "cache-$($_.Name).zip"
echo $cacheFile
if (!(Test-Path $cacheFile)) {
7z a $cacheFile $_.FullName
}
}
- name: Commit and Push Changes to Build Repository
run: |
cd _deps-vs2019-x64-installed
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add *.zip
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || echo "Push failed"
- name: Package .zip Archives
run: |
$VERSION = 'v' + ((Get-Content VERSION).Trim())
$SHA = ${env:GITHUB_SHA}.Substring(0, 7)
$OUTPUT_DIR = "$env:USERPROFILE\output"
New-Item -ItemType Directory -Force -Path $OUTPUT_DIR
if ("${{ matrix.python }}" -eq "3.9.11") {
# only for the first python version the executables are assembled for upload
cd _installed-vs2019-x64/bin
Get-ChildItem -Path . | ForEach-Object {
echo $_
$exe = $_.Name
$baseName = $exe.Substring(0, $exe.Length - 4)
$zipName = "${baseName}-$VERSION-$SHA-win64.zip"
7z a $zipName $exe
}
mv *.zip $OUTPUT_DIR
}
$pyVersion = "${{ matrix.python }}"
$pyVersionMajor = ($pyVersion -split '\.')[0..1] -join ''
cd C:\Python\${{ matrix.python }}\Lib\site-packages
Remove-Item -Recurse -Force ifcopenshell\__pycache__ -ErrorAction SilentlyContinue
Get-ChildItem -Path ifcopenshell -Filter "*.pyc" -Recurse | Remove-Item -Force
$zipName = "ifcopenshell-python-$pyVersionMajor-$VERSION-$SHA-win64.zip"
7z a $zipName ifcopenshell
mv $zipName $OUTPUT_DIR
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload .zip Archives to S3
run: |
aws s3 cp $env:USERPROFILE\output s3://ifcopenshell-builds/ --recursive
@@ -1,73 +0,0 @@
name: ci-black-formatting
on:
push:
pull_request:
env:
PYTHON_VERSION: "3.12"
jobs:
lint-formatting:
runs-on: ubuntu-latest
steps:
- name: Action - checkout repository
uses: actions/checkout@v4.2.2
- name: Action - install python
uses: actions/setup-python@v5.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Step 1 - install dependencies
shell: bash
run: |
python3 -m pip install --upgrade pip
python3 -m pip install 'black>=24.10.0'
# NOTE: This would suffice, however it is less informative in terms of the 3 possible outcomes
# - name: QA Step - check linting
# shell: bash
# id: linting
# run: |
# python3 -m black .
# QA STEP
- name: QA Step - check linting
shell: bash
id: linting
run: |
python3 -m black --check . \
&& exit 0 \
|| (echo "exit_code=$?" >> "$GITHUB_OUTPUT" && exit 1);
continue-on-error: true
# OUTCOME 1 of QA STEP
- name: QA Step - no linting errors
if: steps.linting.outcome == 'success'
shell: bash
run: |-
echo "::notice::QA step linting succeeded"
exit 0;
# OUTCOME 2i of QA STEP
- name: QA Step - unprettified code with no syntax errors
if: steps.linting.outputs.exit_code == 1
shell: bash
run: |-
echo "::group::QA step succeeded with warnings"
echo "::warning::one or more files contains unformatted code but no syntax errors";
echo "::notice::please run the linter before pushing!";
echo "::endgroup::"
exit 0;
# OUTCOME 2ii of QA STEP
- name: QA Step - code contains syntax errors
if: steps.linting.outputs.exit_code == 123
shell: bash
run: |-
echo "::group::QA step failed"
echo "::error::one or more files contains syntax errors";
echo "::notice::please run the linter and fix syntax errors before pushing!";
echo "::endgroup::"
exit 1;
+12
View File
@@ -0,0 +1,12 @@
name: ci-black-formatting
on:
push:
pull_request:
jobs:
lint-formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: psf/black@stable
+20 -42
View File
@@ -99,12 +99,12 @@ jobs:
token: ${{ secrets.IOS_TO_BLENDER_REPO }}
path: bonsai_unstable_repo
- name: Download Blender and run critical tests
- name: Update index.json on extensions repo
run: |
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
set -x -e
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz
wget -q -O blender.tar.xz https://ftp.nluug.nl/pub/graphics/blender/release/Blender4.2/blender-4.2.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
@@ -112,25 +112,25 @@ jobs:
export PATH="$PATH:$BLENDER_PATH"
blender --version
# Setup unstable repo to get Bonsai build.
cd bonsai_unstable_repo
pip install -r requirements.txt
python setup_extensions_repo.py --last-tag
cd ..
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
git add index.json
git add readme.md
git commit -m "Update index.json"
git push
- name: Run bonsai tests
run: |
set -x -e
BLENDER_PATH=$(find blender-*/ -maxdepth 0 -exec readlink -f {} \;)
export PATH="$PATH:$BLENDER_PATH"
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py311*-linux-x64.zip)"
# Install Bonsai.
blender --version
blender --command extension install-file -r user_default -e $bonsai_zip
blender --command extension list
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
# Reregister Bonsai.
# Note that running it in background might miss some errors
# (e.g. tools are not registered in background mode).
blender --background --python IfcOpenShell/src/bonsai/scripts/reregister_bonsai.py
# Install sverchok.
wget -q -O sverchok.zip https://github.com/nortikin/sverchok/archive/refs/heads/master.zip
# ifcsverchok expecting sverchok to be named "sverchok" and not "sverchok-master".
unzip -q sverchok.zip
@@ -139,41 +139,19 @@ jobs:
rm -r sverchok
blender --command extension install-file -r user_default sverchok.zip
# Install ifcsverchok.
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
cd IfcOpenShell/src/ifcsverchok
make dist
sverchok_zip="$(pwd)/dist/$(ls dist)"
blender --command extension install-file -r user_default $sverchok_zip
- name: Update index.json on extensions repo
run: |
set -x -e
# Setup Blender.
BLENDER_PATH=$(find blender-*/ -maxdepth 0 -exec readlink -f {} \;)
export PATH="$PATH:$BLENDER_PATH"
blender --version
cd bonsai_unstable_repo
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
git add index.json
git add readme.md
git commit -m "Update index.json"
git push
- name: Run bonsai tests
run: |
set -x -e
BLENDER_PATH=$(find blender-*/ -maxdepth 0 -exec readlink -f {} \;)
export PATH="$PATH:$BLENDER_PATH"
blender --version
# Install Sun Position extension.
blender --online-mode --command extension sync
blender --online-mode --command extension install --enable --sync sun_position
blender --online-mode --background --python-expr "import bpy; \
bpy.ops.extensions.package_install(repo_index=0, pkg_id='sun_position'); \
bpy.ops.preferences.addon_enable(module='bl_ext.blender_org.sun_position'); bpy.ops.wm.save_userpref()"
cd IfcOpenShell/src/bonsai
cd ../bonsai
pip install pytest-blender
blender --background --python scripts/setup_pytest.py
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
+4
View File
@@ -24,6 +24,10 @@ jobs:
name: "Windows 64bit",
short_name: win64,
}
- {
name: "Windows 32bit",
short_name: win32,
}
- {
name: "Linux 64bit",
short_name: linux64
@@ -25,6 +25,10 @@ jobs:
name: "Windows 64bit",
short_name: win64,
}
- {
name: "Windows 32bit",
short_name: win32,
}
- {
name: "Linux 64bit",
short_name: linux64
+4 -6
View File
@@ -15,7 +15,6 @@ on:
- 'src/ifcparse/**'
- 'src/ifcwrap/**'
- 'src/qtviewer/**'
- 'src/svgfill/**'
- 'src/serializers/**'
- 'conda/**'
- 'cmake/**'
@@ -24,15 +23,15 @@ on:
jobs:
activate:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell' &&
!contains(github.event.head_commit.message, 'skip ci')
steps:
- run: echo ok go
compile-and-test:
runs-on: ubuntu-22.04
build:
runs-on: ubuntu-latest
needs: activate
steps:
- uses: actions/checkout@v4
@@ -48,6 +47,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install mathutils
pip install src/bcf --no-deps
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
@@ -126,7 +126,6 @@ jobs:
python tests.py
cd ../src/ifcopenshell-python
mv ifcopenshell ifcopenshell-local # Force testing on installed module
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
make test
cd ../bcf && make test
pip install requests
@@ -134,5 +133,4 @@ jobs:
pip install deepdiff
cd ../ifcdiff && make test
cd ../ifcpatch && make test
pip install -e ../ifctester --no-deps
cd ../ifctester && make test
@@ -1,38 +0,0 @@
name: Build and Deploy Unstable Documentation
on:
push:
branches:
- v0.8.0 # Trigger the workflow on pushes to the default branch which is currently v0.8.0
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
cd src/bonsai/docs # Navigate to the docs directory
pip install -r requirements.txt # Install dependencies from requirements.txt
- name: Build documentation
run: |
cd src/bonsai/docs # Navigate to the docs directory
make html # Build the documentation
- name: Deploy to GitHub Pages (Unstable)
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} # SSH key for deployment
external_repository: IfcOpenShell/bonsaibim_org_docs_unstable # Target repository
publish_branch: main # Branch to deploy to
cname: docs-unstable.bonsaibim.org # Custom domain for unstable docs
publish_dir: src/bonsai/docs/_build/html # Directory containing built docs
-36
View File
@@ -1,36 +0,0 @@
name: Build and Deploy Stable Documentation
on:
workflow_dispatch: # Manual trigger
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
cd src/bonsai/docs
pip install -r requirements.txt # Run pip install from the docs directory
- name: Build documentation
run: |
cd src/bonsai/docs
make html
- name: Deploy to GitHub Pages (Stable)
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: IfcOpenShell/bonsaibim_org_docs
publish_branch: main
cname: docs.bonsaibim.org
publish_dir: src/bonsai/docs/_build/html
@@ -1,50 +0,0 @@
name: Deploy Pyodide Demo App to GitHub Pages
permissions:
id-token: write
pages: write
on:
push:
paths:
- 'src/pyodide/**'
- '.github/workflows/publish-pyodide-demo-app.yml'
branches:
- v0.8.0
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@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v3
with:
path: src/pyodide/demo-app/
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-9
View File
@@ -81,12 +81,6 @@ src/bonsai/test/files/temp
src/bonsai/test/files/basic.ifc.cache.blend
src/bonsai/test/files/basic.ifc.cache.sqlite
# bonsai data
src/bonsai/bonsai/bim/data/build/
src/bonsai/bonsai/bim/data/gantt/index.html
src/bonsai/bonsai/bim/data/gantt/jsgantt.js
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
src/bonsai/drawings
src/bonsai/layouts
@@ -104,6 +98,3 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# Brickschema
src/bonsai/bonsai/bim/schema/Brick.ttl
bonsaiDecoratorForLoads.code-workspace
dev_environment.bat
-6
View File
@@ -17,9 +17,3 @@
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
path = docs/cpp-api/assets/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
path = src/ifcopenshell-python/ifcopenshell/simple_spf
url = https://github.com/IfcOpenShell/step-file-parser
[submodule "src/pyodide/demo-app/wheels"]
path = src/pyodide/demo-app/wheels
url = https://github.com/IfcOpenShell/wasm-wheels
-1
View File
@@ -6,7 +6,6 @@ IfcOpenShell
<img src="https://github.com/IfcOpenShell/IfcOpenShell/assets/88302/34901387-e2dd-4a0c-8e38-9ffc32a66cde">
</p>
IfcOpenShell is an open source ([LGPL]) software library for working with Industry Foundation Classes ([IFC]). Complete
parsing support is provided for [IFC2x3 TC1], [IFC4 Add2 TC1], IFC4x1, IFC4x2, and [IFC4x3 Add2]. Extensive geometric support
is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending with support for arbitrary IFC schemas
+1 -1
View File
@@ -1 +1 @@
0.8.2
0.8.1
+13 -55
View File
@@ -77,7 +77,7 @@ option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
option(CITYJSON_SUPPORT "Build IfcConvert with CityJSON support (requires CityJSON library)." OFF)
option(CITYJSON_SUPPORT "Build IfcConvert with CityJSON support (requires CityJSON library)." ON)
option(WITH_RELATIONSHIP_VALIDATION "Build IfcConvert with option to validate geometrical relationships." OFF)
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
@@ -196,25 +196,13 @@ foreach(option_flag IN LISTS option_flags)
convert_env_var_to_bool("${option_flag}")
endforeach()
set(CMAKE_FIND_ROOT_PATH_BACKUP "${CMAKE_FIND_ROOT_PATH}")
macro(clear_wasm_sysroot)
if(WASM_BUILD)
# when using the nix/build-all.py build script we should not
# look into the sysroot for most of the dependencies but rather
# in the designated build/ folder created by the script.
set(CMAKE_FIND_ROOT_PATH_BACKUP "${CMAKE_FIND_ROOT_PATH}")
set(CMAKE_FIND_ROOT_PATH "")
endif()
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
endmacro()
macro(restore_wasm_sysroot)
if(WASM_BUILD)
# reset to use sysroot
set(CMAKE_FIND_ROOT_PATH "${CMAKE_FIND_ROOT_PATH_BACKUP}")
endif()
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endmacro()
if(WITH_CGAL)
add_definitions(-DIFOPSH_WITH_CGAL)
@@ -235,9 +223,7 @@ endif()
if(GLTF_SUPPORT OR CITYJSON_SUPPORT)
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
clear_wasm_sysroot()
find_path(json_header_path "nlohmann/json.hpp" HINTS ${JSON_INCLUDE_DIR})
restore_wasm_sysroot()
set(JSON_INCLUDE_DIR ${json_header_path})
if(json_header_path)
@@ -344,17 +330,13 @@ if(USE_MMAP)
add_definitions(-DUSE_MMAP)
endif()
clear_wasm_sysroot()
find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
restore_wasm_sysroot()
message(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}")
message(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
if(NOT MINIMAL_BUILD)
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
clear_wasm_sysroot()
find_package(LibXml2 REQUIRED)
restore_wasm_sysroot()
endif()
if(IFCXML_SUPPORT)
@@ -370,7 +352,6 @@ if(BUILD_IFCGEOM)
# Open CASCADE
if(WITH_OPENCASCADE)
if("${OCC_INCLUDE_DIR}" STREQUAL "")
clear_wasm_sysroot()
find_path(OCC_INCLUDE_DIR Standard_Version.hxx
PATHS
/usr/include/occt
@@ -378,7 +359,6 @@ if(BUILD_IFCGEOM)
/usr/include/opencascade
REQUIRED
)
restore_wasm_sysroot()
if(OCC_INCLUDE_DIR)
message(STATUS "Found Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
@@ -438,9 +418,7 @@ if(BUILD_IFCGEOM)
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
endif()
clear_wasm_sysroot()
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
restore_wasm_sysroot()
if(libTKernel)
message(STATUS "Required Open Cascade Library files found")
@@ -663,6 +641,11 @@ if(HDF5_SUPPORT)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
endif(HDF5_SUPPORT)
if(WASM_BUILD)
# reset to use sysroot
set(CMAKE_FIND_ROOT_PATH "${CMAKE_FIND_ROOT_PATH_BACKUP}")
endif()
if(ENABLE_BUILD_OPTIMIZATIONS)
if(MSVC)
# NOTE: RelWithDebInfo and Release use O2 (= /Ox /Gl /Gy/ = Og /Oi /Ot /Oy /Ob2 /Gs /GF /Gy) by default,
@@ -942,10 +925,8 @@ endif()
if(BUILD_IFCGEOM)
if(WITH_CGAL)
clear_wasm_sysroot()
find_library(libGMP NAMES gmp mpir PATHS ${GMP_LIBRARY_DIR} NO_DEFAULT_PATH)
find_library(libMPFR NAMES mpfr PATHS ${MPFR_LIBRARY_DIR} NO_DEFAULT_PATH)
restore_wasm_sysroot()
if(NOT libGMP)
message(FATAL_ERROR "Unable to find GMP library files, aborting")
endif()
@@ -1148,7 +1129,6 @@ if(ADD_COMMIT_SHA)
message("git found: ${GIT_EXECUTABLE} with version ${GIT_VERSION_STRING}")
execute_process(
COMMAND ${GIT_EXECUTABLE} branch -a --contains HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE git_branches
OUTPUT_STRIP_TRAILING_WHITESPACE
)
@@ -1166,18 +1146,11 @@ if(ADD_COMMIT_SHA)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE git_sha
OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(STATUS "IfcOpenShell branch: \"${git_branch}\"")
message(STATUS "IfcOpenShell commit: \"${git_sha}\"")
if ("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
message(FATAL_ERROR "Unable to determine commit sha and/or branch")
endif()
add_definitions(-DIFCOPENSHELL_BRANCH=${git_branch})
add_definitions(-DIFCOPENSHELL_COMMIT=${git_sha})
endif()
@@ -1228,20 +1201,13 @@ install(TARGETS IfcParse
)
if(BUILD_IFCGEOM)
# install(FILES ${IFCGEOM_H_FILES}
# DESTINATION ${INCLUDEDIR}/ifcgeom
# )
install(FILES ${SCHEMA_AGNOSTIC_H_FILES}
install(FILES ${IFCGEOM_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom
)
foreach(kernel ${GEOMETRY_KERNELS})
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/kernels/${kernel}/*.h)
install(FILES ${IFCGEOM_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom/kernels/${kernel}
)
endforeach()
install(FILES ${SCHEMA_AGNOSTIC_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom_schema_agnostic
)
install(TARGETS ${IFCGEOM_SCHEMA_LIBRARIES} ${kernel_libraries} IfcGeom
ARCHIVE DESTINATION ${LIBDIR}
@@ -1257,23 +1223,15 @@ if(BUILD_CONVERT)
RUNTIME DESTINATION ${BINDIR}
)
install(FILES ${SERIALIZERS_H_FILES}
install(FILES ${SERIALIZERS_FILES}
DESTINATION ${INCLUDEDIR}/serializers/
)
install(FILES ${SERIALIZERS_S_H_FILES}
install(FILES ${SERIALIZERS_S_FILES}
DESTINATION ${INCLUDEDIR}/serializers/schema_dependent
)
endif(BUILD_CONVERT)
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
install(TARGETS geometry_serializer ${geometry_serializer_libraries}
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
# Cmake uninstall target
if(NOT TARGET uninstall)
configure_file(
+9 -23
View File
@@ -48,10 +48,6 @@
# on OS X El Capitan with homebrew: #
# $ brew install git bison autoconf automake libffi cmake #
# #
# on RHEL-related distros: #
# $ yum install git gcc gcc-c++ autoconf bison make cmake #
# mesa-libGL-devel libffi-devel fontconfig-devel bzip2 #
# automake patch #
###############################################################################
import logging
import os
@@ -82,11 +78,11 @@ PROJECT_NAME = "IfcOpenShell"
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.0"]
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1"]
JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.8.1"
BOOST_VERSION = "1.86.0"
BOOST_VERSION = "1.80.0"
PCRE_VERSION = "8.41"
LIBXML2_VERSION = "2.9.11"
SWIG_VERSION = "4.0.2"
@@ -307,7 +303,7 @@ if platform.system() == "Darwin":
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
OCE_LOCATION = f"https://github.com/tpaviot/oce/archive/OCE-{OCE_VERSION}.tar.gz"
BOOST_LOCATION = f"https://github.com/boostorg/boost/releases/download/boost-{BOOST_VERSION}/"
BOOST_LOCATION = f"https://boostorg.jfrog.io/artifactory/main/release/{BOOST_VERSION}/source/"
# Helper functions
@@ -350,7 +346,6 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
run([git, "clone", "--recursive", clone_url, target_dir])
else:
logger.info(f"directory '{target_dir}' already cloned. Pulling latest changes.")
run([git, "-C", target_dir, "fetch", "--all", "--tags", "--force"])
# detect whether we are on a branch and pull
if run([git, "rev-parse", "--abbrev-ref", "HEAD"], cwd=target_dir) != "HEAD":
@@ -465,9 +460,6 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
shutil.copytree(os.path.join(extract_dir, "boost"), os.path.join(DEPS_DIR, "install", f"boost-{BOOST_VERSION}", "boost"))
logger.info(f"\rInstalled {name} \n")
if "diskcleanup" in flags:
shutil.rmtree(build_dir, ignore_errors=True)
cecho("Collecting dependencies:", GREEN)
# Set compiler flags for 32bit builds on 64bit system
@@ -479,7 +471,7 @@ if platform.system() == "Darwin":
ADDITIONAL_ARGS = [f"-mmacosx-version-min={TOOLSET}"] + ADDITIONAL_ARGS
if "wasm" in flags:
ADDITIONAL_ARGS.extend(("-sWASM_BIGINT", "-fwasm-exceptions"))
ADDITIONAL_ARGS.extend(("-sWASM_BIGINT", "-fexceptions"))
# If the linker supports GC sections, set it up to reduce binary file size
# -fPIC is required for the shared libraries to work
@@ -528,7 +520,7 @@ if 'hdf5' in targets:
# not supported
orig = [os.environ[f] for f in compiler_flags]
for f in compiler_flags:
os.environ[f] = re.sub(r"-flto(=\w+)?", "", os.environ[f])
os.environ[f] = re.sub("-flto(=\w+)?", "", os.environ[f])
HDF5_MAJOR = ".".join(HDF5_VERSION.split(".")[:-1])
build_dependency(
@@ -618,7 +610,7 @@ if USE_OCCT and "occ" in targets:
"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off",
f"-D3RDPARTY_FREETYPE_DIR={DEPS_DIR}/install/freetype"
],
download_url = "https://github.com/Open-Cascade-SAS/OCCT",
download_url = "https://git.dev.opencascade.org/repos/occt.git",
download_name = "occt",
download_tool=download_tool_git,
patch=patches,
@@ -738,16 +730,14 @@ if "boost" in targets:
"--with-thread",
"--with-date_time",
"--with-iostreams",
"--with-filesystem",
f"link={LINK_TYPE}",
*toolset,
*map(str_concat("cxxflags"), CXXFLAGS.strip().split(' ')),
*map(str_concat("linkflags"), LDFLAGS.strip().split(' ')),
"stage", "-s", "NO_BZIP2=1"],
download_url=BOOST_LOCATION,
# don't remember what this is, but fail on 1.86
# patch="./patches/boost/boostorg_regex_62.patch",
download_name=f"boost-{BOOST_VERSION}-b2-nodocs.tar.gz"
patch="./patches/boost/boostorg_regex_62.patch",
download_name=f"boost_{BOOST_VERSION_UNDERSCORE}.tar.bz2"
)
if "wasm" in flags:
# only supported on nix for now
@@ -990,11 +980,7 @@ if "IfcOpenShell-Python" in targets:
if platform.system() != "Darwin":
if BUILD_CFG == "Release":
# TODO: This symbol name depends on the Python version?
so = glob.glob(os.path.join(module_dir, "_ifcopenshell_wrapper*.so"))[0]
if "wasm" in flags:
run(['wasm-strip', so, '-k', "dylink.0"])
else:
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", so], cwd=module_dir)
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", glob.glob(os.path.join(module_dir, "_ifcopenshell_wrapper*.so"))[0]], cwd=module_dir)
return module_dir
+2 -3
View File
@@ -3,12 +3,11 @@ package:
version: 0.8.0
source:
path: ../../IfcOpenShell
path: IfcOpenShell
build:
script: |
BUILD_CFG=Release python nix/build-all.py --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py312 IfcOpenShell-Python
mv package/ifcopenshell .
python nix/build-all.py --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py310 IfcOpenShell-Python
cp pyodide/setup.py .
about:
+2 -2
View File
@@ -1,11 +1,11 @@
from setuptools import setup, find_packages
setup(name='ifcopenshell',
setup(name='IfcOpenShell',
version='0.8.0',
description='IfcOpenShell is an open source (LGPL) software library for working with the Industry Foundation Classes (IFC) file format.',
author='Thomas Krijnen',
author_email='thomas@aecgeeks.com',
url='http://ifcopenshell.org',
packages=find_packages(),
package_data={'': ['*.so', '*.json']},
package_data={'': ['*.so']},
)
-2
View File
@@ -27,9 +27,7 @@ include = '''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/*
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|src/ifc2ca/templates/*
|src/ifcconvert/cityjson/*
'''
[tool.pyright]
+51 -27
View File
@@ -46,7 +46,6 @@ VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
PYVERSION:=py310
PYPI_IMP:=cp
@@ -86,7 +85,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=c49ca69
OLD:=0e5008d
.PHONY: bump
bump:
ifndef NEW
@@ -178,8 +177,6 @@ endif
# Is not platform specific but has platform specific dependencies.
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download python-socketio[asyncio_client] $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download aiohttp $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
# Required to access platform specific paths
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download platformdirs --dest=./wheels
# Required by light module
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pytz --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download tzfpy $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
@@ -215,7 +212,7 @@ endif
# Brickschema requires pkg_resources which is provided by Blender.
# Provides Brickschema functionality
# For now lets bundle the latest nightly schema
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
cd build/bonsai/bim/schema && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
# Required for hipped roof generation
cd build && wget https://github.com/prochitecture/bpypolyskel/archive/refs/heads/master.zip
@@ -229,16 +226,8 @@ endif
)" bdist_wheel
cp -r build/bpypolyskel-master/dist/*.whl build/wheels/
# folder for executable files
mkdir -p build/bonsai/libs/bin
# required for three-way git merging
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 && unzip ifcmerge.zip && rm ifcmerge.zip
else
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge
endif
# Required for Desktop icon and file association
cp -r bonsai/libs/desktop build/bonsai/libs/
# Generate translations module for Bonsai build
git clone https://github.com/IfcOpenShell/bonsai-translations.git build/working
@@ -255,7 +244,6 @@ ifeq ($(IS_STABLE), TRUE)
else
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif
@@ -280,11 +268,54 @@ endif
fi
mv build/wheels/*.whl build/bonsai/wheels/
# Temporary workaround for Blender not handling non-3.11 wheels #5743.
# Use '-n' as on macos x86-64 doesn't have a binary wheel and it autoincludes 'cp311'.
# Ugly patch to accomodate MacOS universal builds not being recognized by Blender.
# Blender doesn't look into the wheel contents, it just checks the name.
# So this hack works without working out some way to repack those wheels.
# See: https://projects.blender.org/blender/blender/issues/125091
ifeq ($(PLATFORM), macosm1)
# Has universal and x86_64 builds.
prev_whl_name=$$(find build/bonsai/wheels/lxml-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_arm64/"); \
mv "$$prev_whl_name" "$$whl_name";
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/-cp39-/-cp$(PYNUMBER)-/"); \
mv --update=none "$$prev_whl_name" "$$whl_name";
whl_name=$$(echo $$prev_whl_name | sed -E "s/macosx_10_([0-9]+)_x86_64\.macosx_11_0_arm64\.macosx_10_\1_universal2/macosx_10_\1_arm64/"); \
mv "$$prev_whl_name" "$$whl_name";
# Has only universal build.
prev_whl_name=$$(find build/bonsai/wheels/greenlet-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_arm64/"); \
mv "$$prev_whl_name" "$$whl_name";
endif
ifeq ($(PLATFORM), macos)
# Has universal and arm64 builds.
prev_whl_name=$$(find build/bonsai/wheels/MarkupSafe-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_x86_64/"); \
mv "$$prev_whl_name" "$$whl_name";
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed -E "s/macosx_10_([0-9]+)_x86_64\.macosx_11_0_arm64\.macosx_10_\1_universal2/macosx_10_\1_x86_64/"); \
mv "$$prev_whl_name" "$$whl_name";
prev_whl_name=$$(find build/bonsai/wheels/greenlet-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_x86_64/"); \
mv "$$prev_whl_name" "$$whl_name";
# has universal and arm builds.
prev_whl_name=$$(find build/bonsai/wheels/fonttools-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_x86_64/"); \
mv "$$prev_whl_name" "$$whl_name";
endif
# Safeguard for unhandled universal files.
wheels=$$(find build/bonsai/wheels/*universal2*.whl); \
if [ -n "$$wheels" ]; then \
echo "Found universal2 wheel files:"; \
echo "$$wheels"; \
echo "Error: universal2 wheel files are not supported by Blender!"; \
exit 1; \
fi
ifneq ($(PLATFORM), linux)
# Safeguard: in case one of `pip download` will break,
@@ -302,7 +333,6 @@ endif
rm -rf build/bonsai/bim/
rm -rf build/bonsai/core/
rm -rf build/bonsai/tool/
rm -rf build/bonsai/libs/
ifeq ($(IS_STABLE), TRUE)
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-$(BLENDER_PLATFORM).zip ./bonsai
@@ -346,12 +376,6 @@ else
pytest test/tool/test_$(MODULE).py
endif
# Reregistering test is not added to the standard test suite because during unregister
# Blender removes all Bonsai dependencies breaking dev-environment symlinks.
.PHONY: test-reregister
test-reregister:
blender --python scripts/reregister_bonsai.py
.PHONY: qa
qa:
black .
Binary file not shown.
+2 -17
View File
@@ -40,7 +40,6 @@ from typing import Union, Any, Generator
last_commit_hash = "8888888"
last_commit_date = "9999999"
def get_last_commit_hash() -> Union[str, None]:
@@ -52,12 +51,6 @@ def get_last_commit_hash() -> Union[str, None]:
return last_commit_hash[:7]
def get_last_commit_date() -> Union[str, None]:
if last_commit_date == str(9_999999):
return None
return last_commit_date
# Accessed from bonsai extension:
bbim_semver: dict[str, Any] = {}
@@ -103,7 +96,6 @@ def get_debug_info():
"blender_version": bpy.app.version_string,
"bonsai_version": bbim_version,
"bonsai_commit_hash": get_last_commit_hash(),
"bonsai_commit_date": get_last_commit_date(),
"last_actions": last_actions,
"last_error": last_error,
}
@@ -226,7 +218,6 @@ if IN_BLENDER:
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
@@ -326,7 +317,7 @@ if IN_BLENDER:
# But Blender currently doesn't support separate builds for different Python version,
# so those issues might still slip in.
box.label(text="Bonsai installed for wrong Python version.")
box.label(text=f"Expected binary version: {py}. Got: {binary_py}.")
box.label(text=f"Expected: {py}. Got: {binary_py}.")
# On reinstallation, dependencies versions doesn't change, so Blender will just ignore new dependencies.
# So, we need to make user will disable an extension (just uninstallation won't remove dependencies).
# Blender restart doesn't seem to be required in that case
@@ -334,13 +325,7 @@ if IN_BLENDER:
box.label(text="Try reinstalling with the correct Python version.")
box.label(text="Before reinstallation make sure to")
box.label(text="DISABLE Bonsai (uninstallation won't help).")
if py == "3.11":
box.label(text="You can download correct version below.")
else:
box.label(text="Since you're using Python >3.11,")
box.label(text="installation from Blender extensions platform")
box.label(text="is not supported and you need to download")
box.label(text="and install Bonsai from the link below.")
box.label(text="You can download correct version below.")
layout.operator("bim.copy_debug_information", text="Copy Error Message To Clipboard")
op = layout.operator("bim.open_uri", text="How Can I Fix This?")
+2
View File
@@ -0,0 +1,2 @@
# addon writes tmp stuff directly to its dir
/data/
+4 -18
View File
@@ -20,7 +20,8 @@ import os
import bpy
import bpy.utils.previews
import importlib
from . import handler, ui, prop, operator
from pathlib import Path
from . import handler, ui, prop, operator, helper
from typing import Callable, Union
try:
@@ -83,7 +84,6 @@ modules = {
"covering": None,
"web": None,
"light": None,
"alignment": None,
# Uncomment this line to enable loading of the demo module. Happy hacking!
# The name "demo" must correlate to a folder name in `bim/module/`.
# "demo": None,
@@ -99,25 +99,23 @@ classes = [
operator.BIM_OT_add_section_plane,
operator.BIM_OT_delete_object,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_entity,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.BIM_OT_multiple_file_selector,
operator.ClippingPlaneCutWithCappings,
operator.CloseBlendWarning,
operator.CloseError,
operator.CopyTextToClipboard,
operator.EditBlenderCollection,
operator.FileAssociate,
operator.FileUnassociate,
operator.OpenPath,
operator.OpenUpstream,
operator.OpenUri,
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
operator.SelectDir,
operator.SelectDataDir,
operator.SelectIfcFile,
operator.SelectSchemaDir,
operator.SelectURIAttribute,
operator.SetTab,
operator.SwitchTab,
@@ -137,8 +135,6 @@ classes = [
prop.BIMMeshProperties,
prop.BIMFacet,
prop.BIMFilterGroup,
prop.BIMSnapProperties,
prop.BIMSnapGroups,
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.BIM_UL_topics,
@@ -194,8 +190,6 @@ classes = [
# TODO: move this somewhere else and clean it up
ui.BIM_PT_section_plane,
ui.BIM_PT_section_with_cappings,
ui.BIM_PT_decorators_overlay,
ui.BIM_PT_snappping,
]
for mod in modules.values():
@@ -228,8 +222,6 @@ def register():
bpy.app.handlers.load_post.append(handler.load_post)
bpy.app.handlers.load_post.append(handler.loadIfcStore)
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
bpy.types.Scene.BIMSnapProperties = bpy.props.PointerProperty(type=prop.BIMSnapProperties)
bpy.types.Scene.BIMSnapGroups = bpy.props.PointerProperty(type=prop.BIMSnapGroups)
bpy.types.Screen.BIMAreaProperties = bpy.props.CollectionProperty(type=prop.BIMAreaProperties)
bpy.types.Screen.BIMTabProperties = bpy.props.PointerProperty(type=prop.BIMTabProperties)
bpy.types.Collection.BIMCollectionProperties = bpy.props.PointerProperty(type=prop.BIMCollectionProperties)
@@ -264,12 +256,6 @@ def register():
icons = icon_preview
bpy.app.translations.register("bonsai", translations_dict)
import bonsai.tool as tool
tool.Blender.ensure_bin_in_path()
# RestrictedContext doesn't allow accessing scene attribute, postpone it for a bit.
bpy.app.timers.register(tool.Blender.setup_user_data_dir, first_interval=0.1)
def unregister():
global icons
@@ -1,3 +0,0 @@
*
!.gitignore
!README.md
@@ -1 +0,0 @@
This directory stores Brickschema definitions in TTL format.
@@ -1,3 +0,0 @@
*
!.gitignore
!README.md
@@ -1 +0,0 @@
This directory stores generated drawings in SVG format.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2025-03-27T17:30:27+05:00',(''),(''),'IfcOpenShell 0.0.0','IfcOpenShell 0.0.0','Nobody');
FILE_NAME('/dev/null','2024-03-03T16:03:58+11:00',(),(),'IfcOpenShell v0.7.0-eaa2aa05a','IfcOpenShell v0.7.0-eaa2aa05a','Nobody');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
@@ -29,7 +29,7 @@ DATA;
#22=IFCMATERIALLAYERSET((#24),$,$);
#23=IFCRELASSOCIATESMATERIAL('0khP_Smdj12f2JrOTRbRKj',$,$,$,(#21),#22);
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#59,#92,#38,#82,#30,#96,#77,#63,#42,#54,#87,#153,#34,#21,#67,#210,#26,#48,#72));
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#210,#153,#34,#67,#38,#82,#77,#42));
#26=IFCWALLTYPE('3R5onkTtX1IxdCMPoThRaw',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#27=IFCMATERIALLAYERSET((#29),$,$);
#28=IFCRELASSOCIATESMATERIAL('0$mXRPCT16nP6Q70XFNvZv',$,$,$,(#26),#27);
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23,#24,#25,#26,#27));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -28,9 +28,5 @@ DATA;
#21=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('28mJ$GDxb7kBbKFH_rACMQ',$,'AngleDecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#24=IFCSIMPLEPROPERTYTEMPLATE('0EP4WR7eb1IR$rqEJ_XRVX',$,'DPI','DPI of rasterized underlays',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#25=IFCSIMPLEPROPERTYTEMPLATE('3LHwCrOcb6Y8ozfJZ7Ay$c',$,'LineworkMode','Method to use for line work',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -1,11 +1,11 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION($,'2;1');
FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Annotation','Psets_BBIM_Annotation',$);
FILE_DESCRIPTION((),'2;1');
FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_Annotation','Psets_BBIM_Annotation',$);
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4,#29));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -28,13 +28,11 @@ DATA;
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30));
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
ENDSEC;
END-ISO-10303-21;
@@ -1,3 +0,0 @@
*
!.gitignore
!README.md
@@ -1 +0,0 @@
This directory stores generated tabular schedules in SVG format.
@@ -1,3 +0,0 @@
*
!.gitignore
!README.md
@@ -1 +0,0 @@
This directory stores generated sheets in SVG format. Sheets typically contain drawings and schedules.
@@ -1 +0,0 @@
../../libraries/IFC2X3 Demo Library.ifc
@@ -1 +0,0 @@
../../libraries/IFC4 Demo Library.ifc
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
../../libraries/IFC4X3 Demo Library.ifc
@@ -18,7 +18,11 @@ import xml.etree.ElementTree as ET
sio_port = 8080 # default port
sio = socketio.AsyncServer(cors_allowed_origins="*", async_mode="aiohttp", max_http_buffer_size=10000000)
sio = socketio.AsyncServer(
cors_allowed_origins="*",
async_mode="aiohttp",
max_http_buffer_size=10000000
)
app = web.Application()
sio.attach(app)
@@ -269,7 +269,13 @@ function addTableElement(blenderId, csvData, filename) {
},
});
createDownloadIcon(blenderId);
var downloadCsv = $('<a><i class="fa-solid fa-file-csv"></i></a>')
.css("margin-left", "10px")
.css("cursor", "pointer");
tableTitle.append(downloadCsv);
downloadCsv.on("click", function () {
table.download("csv", "data.csv");
});
table.on("rowSelected", function (row) {
var index = row.getIndex(); // the guid of the object
@@ -295,18 +301,6 @@ function addTableElement(blenderId, csvData, filename) {
});
}
function createDownloadIcon(blenderId) {
const table = Tabulator.findTable("#table-" + blenderId)[0];
const tableTitle = $("#title-" + blenderId);
var downloadCsv = $('<a><i class="fa-solid fa-file-csv"></i></a>')
.css("margin-left", "10px")
.css("cursor", "pointer");
tableTitle.append(downloadCsv);
downloadCsv.on("click", function () {
table.download("csv", "data.csv");
});
}
// Function to update table and filename
function updateTableElement(blenderId, csvData, filename) {
// check if the headers are the same
@@ -335,7 +329,6 @@ function updateTableElement(blenderId, csvData, filename) {
}
}
$("#title-" + blenderId).text(filename);
createDownloadIcon(blenderId);
}
}
+17 -17
View File
@@ -20,6 +20,7 @@ from __future__ import annotations
import os
import bpy
import json
import numpy as np
import datetime
import zipfile
import tempfile
@@ -48,7 +49,6 @@ class IfcExporter:
IfcStore.update_cache()
self.sync_all_objects()
self.sync_edited_objects()
tool.Project.save_linked_models_to_ifc()
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
if extension == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
@@ -115,12 +115,17 @@ class IfcExporter:
for obj in IfcStore.edited_objs.copy():
if not obj:
continue
if not tool.Blender.is_valid_data_block(obj):
continue
element = tool.Ifc.get_entity(obj)
if element:
results.append(element)
bpy.ops.bim.update_representation(obj=obj.name)
try:
if isinstance(obj, bpy.types.Material):
# TODO: do we add materials to edited_objs?
continue
else:
element = tool.Ifc.get_entity(obj)
if element:
results.append(element)
bpy.ops.bim.update_representation(obj=obj.name)
except ReferenceError:
pass # The object is likely deleted
IfcStore.edited_objs.clear()
return results
@@ -132,12 +137,13 @@ class IfcExporter:
bpy.ops.bim.update_representation(obj=obj.name)
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
mprops = tool.Geometry.get_mesh_props(obj.data)
checksum = mprops.material_checksum
checksum = obj.data.BIMMeshProperties.material_checksum
return checksum != tool.Geometry.get_material_checksum(obj)
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.is_moved(obj):
return
if tool.Geometry.is_scaled(obj):
bpy.ops.bim.update_representation(obj=obj.name)
# update_representation might not apply scale if the object has openings
@@ -145,13 +151,7 @@ class IfcExporter:
if tool.Geometry.is_scaled(obj):
print(f"WARNING. Object '{obj.name}' scales ({obj.scale[:]}) are reset during project save.")
obj.scale = (1.0, 1.0, 1.0)
else:
# Return and don't handle is_moved as
# updata_representation will run edit_object_placement if object is scaled
# and had no openings.
return element
if not tool.Ifc.is_moved(obj):
return
return element
if element.is_a("IfcGridAxis"):
return self.sync_grid_axis_object_placement(obj, element)
if not hasattr(element, "ObjectPlacement"):
+42 -90
View File
@@ -24,15 +24,14 @@ import ifcopenshell.util.unit
import ifcopenshell.api.owner.settings
import bonsai.bim
import bonsai.tool as tool
import bonsai.core.owner as core_owner
from bpy.app.handlers import persistent
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from bonsai.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
from mathutils import Vector
from math import cos
from math import cos, degrees
from typing import Union, Callable
@@ -53,24 +52,19 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
return
if isinstance(obj, bpy.types.Material):
props = tool.Style.get_material_style_props(obj)
if ifc_definition_id := props.ifc_definition_id:
if props.is_renaming:
props.is_renaming = False
return
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id:
IfcStore.get_file().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
return
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
if not obj.BIMObjectProperties.ifc_definition_id:
return
if props.is_renaming:
props.is_renaming = False
if obj.BIMObjectProperties.is_renaming:
obj.BIMObjectProperties.is_renaming = False
return
element = tool.Ifc.get().by_id(props.ifc_definition_id)
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
if "/" in obj.name:
object_name = obj.name
element_name = obj.name.split("/", 1)[1]
@@ -86,8 +80,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
if not element.is_a("IfcRoot"):
return
element.Name = element_name
if props.collection:
props.collection.name = object_name
if obj.BIMObjectProperties.collection:
obj.BIMObjectProperties.collection.name = object_name
refresh_ui_data()
@@ -106,44 +100,29 @@ def update_bim_tool_props():
return
mode = bpy.context.mode
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode)
if not current_tool or current_tool.idname not in tool.Blender.get_list_of_tools():
if not current_tool or current_tool.idname not in LIST_OF_TOOLS:
return
element = tool.Ifc.get_entity(obj)
if not element:
return
props = tool.Model.get_model_props()
aprops = tool.Drawing.get_annotation_props()
TOOLS_TO_CLASSES_MAP = tool.Blender.get_tools_to_classes_map()
is_annotation_tool = current_tool.idname == "bim.annotation_tool"
if element.is_a("IfcTypeProduct") or element.is_a("IfcProduct"):
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
is_bim_tool = current_tool.idname == "bim.bim_tool"
if is_annotation_tool and (object_type := tool.Drawing.get_annotation_type_object_type(element_type)):
aprops.object_type = object_type
aprops.relating_type_id = str(element_type.id())
return
if is_bim_tool:
props.ifc_class = element_type.is_a()
if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a():
props.relating_type_id = str(element_type.id())
if is_annotation_tool:
return
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
props = bpy.context.scene.BIMModelProperties
if element.is_a("IfcElementType") or element.is_a("IfcElement"):
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
is_bim_tool = current_tool.idname == "bim.bim_tool"
if is_bim_tool:
props.ifc_class = element_type.is_a()
if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a():
props.relating_type_id = str(element_type.id())
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
def get_x_angle(extrusion: ifcopenshell.entity_instance) -> float:
def get_x_angle(extrusion):
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
return x_angle
@@ -155,7 +134,7 @@ def update_bim_tool_props():
if AuthoringData.data["active_material_usage"] == "LAYER2":
x_angle = get_x_angle(extrusion)
axis = tool.Model.get_wall_axis(obj)["reference"]
props.extrusion_depth = abs(extrusion.Depth * si_conversion * cos(x_angle))
props.extrusion_depth = extrusion.Depth * si_conversion * cos(x_angle)
props.length = (axis[1] - axis[0]).length
props.x_angle = x_angle
@@ -201,9 +180,6 @@ def refresh_ui_data():
and it need to be refreshed manually if needed.
"""
from bonsai.bim import modules
import bonsai.bim.ui
bonsai.bim.ui.refresh()
for name, value in modules.items():
try:
@@ -220,38 +196,35 @@ def refresh_ui_data():
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
tool.Ifc.get().clear_cache()
props = tool.Drawing.get_document_props()
props.should_draw_decorations = props.should_draw_decorations
bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations
if bpy.context.scene.WebProperties.is_connected:
tool.Web.send_webui_data()
@persistent
def loadIfcStore(scene: bpy.types.Scene) -> None:
def loadIfcStore(scene):
IfcStore.purge()
refresh_ui_data()
if not tool.Ifc.get():
if not IfcStore.get_file():
return
tool.Ifc.schema()
IfcStore.get_schema()
IfcStore.relink_all_objects()
@persistent
def undo_post(scene: bpy.types.Scene) -> None:
props = tool.Blender.get_bim_props()
if IfcStore.last_transaction != props.last_transaction:
IfcStore.last_transaction = props.last_transaction
IfcStore.undo(until_key=props.last_transaction)
def undo_post(scene):
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction)
refresh_ui_data()
tool.Ifc.rebuild_element_maps()
@persistent
def redo_post(scene: bpy.types.Scene) -> None:
props = tool.Blender.get_bim_props()
if IfcStore.last_transaction != props.last_transaction:
IfcStore.last_transaction = props.last_transaction
IfcStore.redo(until_key=props.last_transaction)
def redo_post(scene):
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction)
refresh_ui_data()
tool.Ifc.rebuild_element_maps()
@@ -284,10 +257,10 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
return pao
def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
def viewport_shading_changed_callback(area):
shading = area.spaces.active.shading.type
if shading == "RENDERED":
tool.Style.get_style_props().active_style_type = "External"
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
def subscribe_to_viewport_shading_changes():
@@ -332,6 +305,8 @@ def load_post(scene):
# After appending the workspace to ensure BIM viewport is affected.
subscribe_to_viewport_shading_changes()
bpy.ops.bim.override_escape("INVOKE_DEFAULT")
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
@@ -342,27 +317,4 @@ def load_post(scene):
tool.Blender.setup_tabs()
if tool.Ifc.get() and bpy.data.is_saved:
props = tool.Blender.get_bim_props()
props.has_blend_warning = True
# Bonsai overlays
georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = tool.Model.get_model_props()
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
if aggregate_props.aggregate_decorator:
AggregateDecorator.install(bpy.context)
if nest_props.nest_decorator:
NestDecorator.install(bpy.context)
if model_props.show_wall_axis:
WallAxisDecorator.install(bpy.context)
if model_props.show_slab_direction:
SlabDirectionDecorator.install(bpy.context)
if scene := bpy.context.scene:
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
scene.tool_settings.use_snap = True
# Match default Bonsai snaps
scene.tool_settings.snap_elements_base = {"EDGE", "EDGE_PERPENDICULAR", "VERTEX", "EDGE_MIDPOINT", "FACE"}
bpy.context.scene.BIMProperties.has_blend_warning = True
+34 -95
View File
@@ -20,19 +20,21 @@ from __future__ import annotations
import importlib
import bpy
import json
import math
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import ifcopenshell.util.unit
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
from mathutils import geometry
from mathutils import Vector
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
if TYPE_CHECKING:
import bonsai.bim.prop
from bonsai.bim.prop import Attribute
from bonsai.bim.module.search.prop import BIMFilterGroup
# ImportCallback return values:
# - None - property should be imported by default workflow
@@ -46,15 +48,12 @@ if TYPE_CHECKING:
def draw_attributes(
props: bpy.types.bpy_prop_collection_idprop[Attribute],
props: list[bonsai.bim.prop.Attribute],
layout: bpy.types.UILayout,
copy_operator: Optional[str] = None,
popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None,
callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None,
) -> None:
"""Draw editable UI for prop.Attributes.
You can set attribute active in popup with `active_attribute`
"""you can set attribute active in popup with `active_attribute`
meaning you will be able to type into attribute's field without having to click
on it first
"""
@@ -63,21 +62,22 @@ def draw_attributes(
if attribute == popup_active_attribute:
row.activate_init = True
draw_attribute(attribute, row, copy_operator)
if callback:
callback(attribute, row)
def draw_attribute(
attribute: bonsai.bim.prop.Attribute, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
) -> None:
value_name = attribute.get_value_name(display_only=True)
value_name = attribute.get_value_name()
if not value_name:
layout.label(text=attribute.name)
return
if value_name == "enum_value":
prop_with_search(layout, attribute, "enum_value", text=attribute.name)
elif value_name == "filepath_value":
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
props = tool.Sequence.get_work_schedule_props()
for item in props.durations_attributes:
elif attribute.name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"]:
propis = bpy.context.scene.BIMWorkScheduleProperties
for item in propis.durations_attributes:
if item.name == attribute.name:
duration_props = item
layout.label(text=attribute.name)
@@ -92,42 +92,37 @@ def draw_attribute(
layout.prop(
attribute,
value_name,
text=attribute.display_name,
text=attribute.name,
)
if attribute.is_uri:
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.data_path = attribute.path_from_id("string_value")
elif attribute.special_type in ("DATE", "DATETIME"):
op = layout.operator("bim.datepicker", text="", icon="TIME")
op.target_prop = attribute.path_from_id("string_value")
op.include_time = attribute.special_type == "DATETIME"
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if attribute.name == "GlobalId":
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
elif copy_operator:
if copy_operator:
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
op.name = attribute.name
def import_attributes(
ifc_class: str,
props: bpy.types.bpy_prop_collection_idprop[Attribute],
props: bpy.types.bpy_prop_collection,
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(ifc_class).all_attributes():
for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
import_attribute(attribute, props, data, callback=callback)
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
props: bpy.types.bpy_prop_collection_idprop[Attribute],
props: bpy.types.bpy_prop_collection,
callback: Optional[ImportCallback] = None,
) -> None:
if isinstance(element, str):
@@ -143,7 +138,7 @@ def import_attributes2(
def import_attribute(
attribute: W.attribute,
props: bpy.types.bpy_prop_collection_idprop[Attribute],
props: bpy.types.bpy_prop_collection,
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
@@ -152,7 +147,7 @@ def import_attribute(
if isinstance(data_type, tuple) or data_type == "entity":
callback(attribute.name(), None, data) if callback else None
return
new: bonsai.bim.prop.Attribute = props.add()
new = props.add()
new.name = attribute.name()
new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional()
@@ -169,57 +164,31 @@ def import_attribute(
new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n")
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
new.is_uri = True
elif attribute.type_of_attribute()._is("IfcDate"):
new.special_type = "DATE"
elif attribute.type_of_attribute()._is("IfcDateTime"):
new.special_type = "DATETIME"
elif data_type == "boolean":
new.bool_value = False if new.is_null else bool(data[attribute.name()])
elif data_type == "integer":
new.int_value = 0 if new.is_null else int(data[attribute.name()])
elif data_type == "float":
attribute_type = attribute.type_of_attribute()
if attribute_type._is("IfcLengthMeasure"):
new.special_type = "LENGTH"
elif attribute_type._is("IfcForceMeasure"):
new.special_type = "FORCE"
new.float_value = 0.0 if new.is_null else float(data[attribute.name()])
elif data_type == "enum":
attribute_type = attribute.type_of_attribute()
is_logical = str(attribute_type) == "<type IfcLogical: <logical>>"
enum_value = data[new.name]
if is_logical:
new.special_type = "LOGICAL"
enum_items = ("TRUE", "FALSE", "UNKNOWN")
new.enum_items = json.dumps(enum_items)
if enum_value is not None and enum_value != "UNKNOWN":
# IfcOpenShell returns bool if IfcLogical is True/False.
enum_value = "TRUE" if enum_value else "FALSE"
else:
enum_items = ifcopenshell.util.attribute.get_enum_items(attribute)
new.enum_items = json.dumps(enum_items)
add_attribute_enum_items_descriptions(new, enum_items)
if enum_value is not None:
new.enum_value = enum_value
enum_items = ifcopenshell.util.attribute.get_enum_items(attribute)
new.enum_items = json.dumps(enum_items)
add_attribute_enum_items_descriptions(new, enum_items)
if data[new.name]:
new.enum_value = data[new.name]
add_attribute_description(new, data)
add_attribute_min_max(attribute, new)
add_attribute_min_max(new)
ATTRIBUTE_MIN_MAX_CONSTRAINTS = {"IfcMaterialLayer": {"Priority": {"value_min": 0, "value_max": 100}}}
def add_attribute_min_max(attribute: W.attribute, attribute_blender: bonsai.bim.prop.Attribute) -> None:
def add_attribute_min_max(attribute_blender: bonsai.bim.prop.Attribute) -> None:
if attribute_blender.ifc_class in ATTRIBUTE_MIN_MAX_CONSTRAINTS:
constraints = ATTRIBUTE_MIN_MAX_CONSTRAINTS[attribute_blender.ifc_class].get(attribute_blender.name, {})
for constraint, value in constraints.items():
setattr(attribute_blender, constraint, value)
setattr(attribute_blender, constraint + "_constraint", True)
attribute_type = attribute.type_of_attribute()
if attribute_type._is("IfcPositiveLengthMeasure") or attribute_type._is("IfcNonNegativeLengthMeasure"):
attribute_blender.value_min = 0.0
attribute_blender.value_min_constraint = True
def add_attribute_enum_items_descriptions(
@@ -257,7 +226,7 @@ def add_attribute_description(attribute_blender: bonsai.bim.prop.Attribute, attr
def export_attributes(
props: bpy.types.bpy_prop_collection_idprop[Attribute],
props: bpy.types.bpy_prop_collection,
callback: Optional[ExportCallback] = None,
) -> dict[str, Any]:
attributes: dict[str, Any] = {}
@@ -272,31 +241,14 @@ def export_attributes(
ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties]
def get_display_value(value: str, float_decimal_precision: int = 6) -> str:
"""
This will get rid of the floating point precision artifacts in float values stored as a string
"""
try:
digits = len(value.split(".")[1])
value = float(value)
if digits > 6: # Maximal decimal float precision
value = round(value, float_decimal_precision)
except (ValueError, IndexError): # Not castable to a float or no decimal places (eg integer)
pass
return str(value)
def prop_with_search(
layout: bpy.types.UILayout,
data: ENUM_ITEMS_DATA,
prop_name: str,
should_click_ok: bool = False,
should_click_ok_to_validate: bool = False,
original_operator_path: Optional[str] = None,
**kwargs: Any,
) -> bpy.types.UILayout:
"""
:return: Added row.
"""
):
# kwargs are layout.prop arguments (text, icon, etc.)
row = layout.row(align=True)
row.prop(data, prop_name, **kwargs)
@@ -306,11 +258,10 @@ def prop_with_search(
row.context_pointer_set(name="data", data=data)
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
op.prop_name = prop_name
op.should_click_ok = should_click_ok
op.should_click_ok_to_validate = should_click_ok_to_validate
op.original_operator_path = original_operator_path or ""
except TypeError: # Prop is not iterable
pass
return row
def get_enum_items(
@@ -351,13 +302,6 @@ def get_enum_items(
return items
def draw_expandable_panel(layout, context, label: str, ui_func, default_closed: bool = True):
header, panel = layout.panel(label, default_closed=default_closed)
header.label(text=label)
if panel:
ui_func(panel, context)
def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip_props: tuple[str, ...] = ()) -> None:
"""Method converts property group values from si to current ifc project units
@@ -377,16 +321,11 @@ def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip
setattr(property_group, prop_name, prop_value)
def draw_filter(
layout: bpy.types.UILayout,
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup],
data,
module: str,
) -> None:
def draw_filter(layout: bpy.types.UILayout, filter_groups, data, module: str) -> None:
if not data.is_loaded:
data.load()
sprops = tool.Search.get_search_props()
sprops = bpy.context.scene.BIMSearchProperties
if tool.Ifc.get():
row = layout.row(align=True)
+28 -88
View File
@@ -45,14 +45,10 @@ class OperationData(TypedDict):
obj: str
class EditObjectOperationData(TypedDict):
obj: str
class Operation(TypedDict):
rollback: Callable
commit: Callable
data: Union[OperationData, EditObjectOperationData, None]
data: Union[OperationData, None]
class TransactionStep(TypedDict):
@@ -105,8 +101,9 @@ class IfcStore:
@staticmethod
def get_file():
if IfcStore.file is None:
props = tool.Blender.get_bim_props()
IfcStore.set_path(props.ifc_file)
IfcStore.path = bpy.context.scene.BIMProperties.ifc_file
if not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
if IfcStore.path:
try:
IfcStore.load_file(IfcStore.path)
@@ -114,21 +111,12 @@ class IfcStore:
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
return IfcStore.file
@staticmethod
def set_path(value):
IfcStore.path = value
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
@staticmethod
def get_cache():
if IfcStore.cache is None and IfcStore.path:
props = tool.Blender.get_bim_props()
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
os.makedirs(props.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
@@ -168,8 +156,7 @@ class IfcStore:
assert IfcStore.file
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
props = tool.Blender.get_bim_props()
new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
@@ -185,7 +172,6 @@ class IfcStore:
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
props = tool.Project.get_project_props()
if extension.lower() == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
with zipfile.ZipFile(path, "r") as zip_ref:
@@ -195,7 +181,7 @@ class IfcStore:
return
elif extension.lower() == "ifcxml":
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
elif props.should_stream:
elif bpy.context.scene.BIMProjectProperties.should_stream:
IfcStore.file = ifcopenshell.open(path, should_stream=True)
else:
IfcStore.file = ifcopenshell.open(path)
@@ -203,8 +189,9 @@ class IfcStore:
@staticmethod
def get_schema() -> ifcopenshell.ifcopenshell_wrapper.schema_definition:
if IfcStore.file is None:
props = tool.Project.get_project_props()
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(props.export_schema)
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(
bpy.context.scene.BIMProjectProperties.export_schema
)
elif IfcStore.schema is None:
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema_identifier)
return IfcStore.schema
@@ -254,7 +241,7 @@ class IfcStore:
# refactor this class and deprecate usage of IfcStore in favour of
# tools.
if not isinstance(obj, (bpy.types.Object, bpy.types.Material)):
tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id()
obj.BIMMeshProperties.ifc_definition_id = element.id()
return
existing_obj = IfcStore.id_map.get(element.id(), None)
@@ -269,11 +256,9 @@ class IfcStore:
IfcStore.guid_map[global_id] = obj
if element.is_a("IfcSurfaceStyle"):
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = element.id()
obj.BIMStyleProperties.ifc_definition_id = element.id()
else:
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = element.id()
obj.BIMObjectProperties.ifc_definition_id = element.id()
tool.Ifc.setup_listeners(obj)
@@ -310,28 +295,7 @@ class IfcStore:
if "guid" in data:
IfcStore.guid_map[data["guid"]] = obj
tool.Ifc.setup_listeners(obj)
@staticmethod
def history_edit_object(obj: bpy.types.Object, *, finish_editing: bool) -> None:
if not IfcStore.history:
return
commit, rollback = IfcStore.commit_edit_object, IfcStore.rollback_edit_object
if finish_editing:
commit, rollback = rollback, commit
data = EditObjectOperationData(obj=obj.name)
IfcStore.history[-1]["operations"].append(Operation(rollback=rollback, commit=commit, data=data))
@staticmethod
def commit_edit_object(data: EditObjectOperationData) -> None:
obj = bpy.data.objects[data["obj"]]
IfcStore.edited_objs.add(obj)
@staticmethod
def rollback_edit_object(data: EditObjectOperationData) -> None:
obj = bpy.data.objects[data["obj"]]
IfcStore.edited_objs.discard(obj)
# TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems.
@staticmethod
def rollback_unlink_element(data: OperationData) -> None:
@@ -411,24 +375,18 @@ class IfcStore:
@staticmethod
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
if isinstance(obj, bpy.types.Material):
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = 0
obj.BIMStyleProperties.ifc_definition_id = 0
else: # bpy.types.Object
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
obj.BIMObjectProperties.ifc_definition_id = 0
# NOTE: in theory this will also remove listeners added by other addons
# though never had a report when this would be a problem.
bpy.msgbus.clear_by_owner(obj)
@staticmethod
def execute_ifc_operator(
operator: tool.Ifc.Operator,
context: bpy.types.Context,
event=None,
method: Literal["EXECUTE", "INVOKE", "MODAL"] = "EXECUTE",
) -> set[str]:
def execute_ifc_operator(operator: tool.Ifc.Operator, context: bpy.types.Context, is_invoke=False) -> set[str]:
bonsai.last_actions.append({"type": "operator", "name": operator.bl_idname})
props = tool.Blender.get_bim_props()
props.is_dirty = True
# Modals don't nest, and Blender handles the loop that continuously calls modal()
is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL")
bpy.context.scene.BIMProperties.is_dirty = True
is_top_level_operator = not bool(IfcStore.current_transaction)
if is_top_level_operator:
IfcStore.begin_transaction(operator)
@@ -454,12 +412,10 @@ class IfcStore:
bonsai.bim.handler.refresh_ui_data()
try:
if method == "EXECUTE":
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
elif method == "INVOKE":
result = getattr(operator, "_invoke")(context, event)
elif method == "MODAL":
result = getattr(operator, "_modal")(context, event)
except:
bonsai.last_error = traceback.format_exc()
# Try to ensure undo will work since Blender undo does work in case of errors.
@@ -471,27 +427,12 @@ class IfcStore:
end_top_level_operator()
raise
if method == "MODAL":
if result == {"FINISHED"}:
end_top_level_operator()
elif result == {"CANCELLED"}:
# Please read the docs: https://docs.blender.org/api/current/bpy.types.Operator.html
# > "when an operator returns {'CANCELLED'}, no undo step will be created".
# This means that if your modal edits IFC data, then the user
# cancels it, Blender's undo history will not be in sync with
# Bonsai / IfcOpenShell's undo history. Instead of hoping for
# Bonsai devs to remember to handle the "cancel" state (i.e.
# detect escape keypress) and return {"FINISHED"}, we instead
# always enforce an undo step.
bpy.ops.ed.undo_push(message=f"Cancel {operator.bl_idname}")
end_top_level_operator()
else:
end_top_level_operator()
end_top_level_operator()
return result
@staticmethod
def begin_transaction(operator: tool.Ifc.Operator) -> None:
IfcStore.current_transaction = str(uuid.uuid4()) + operator.__class__.__name__
IfcStore.current_transaction = str(uuid.uuid4())
operator.transaction_key = IfcStore.current_transaction
@staticmethod
@@ -505,8 +446,7 @@ class IfcStore:
) -> None:
key = getattr(operator, "transaction_key", None)
data = getattr(operator, "transaction_data", None)
props = tool.Blender.get_bim_props()
props.last_transaction = key
bpy.context.scene.BIMProperties.last_transaction = key
IfcStore.last_transaction = key
rollback = rollback or getattr(operator, "rollback", lambda data: True)
commit = commit or getattr(operator, "commit", lambda data: True)
+163 -155
View File
@@ -20,12 +20,9 @@ from __future__ import annotations
import bpy
import time
import json
import ifcpatch
import logging
import traceback
import mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
@@ -36,10 +33,10 @@ import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import bonsai.tool as tool
from itertools import chain, accumulate
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any, Literal
from ifcopenshell.util.shape import MatrixType
from typing import Dict, Union, Optional, Any
class MaterialCreator:
@@ -71,38 +68,19 @@ class MaterialCreator:
if isinstance(mesh, bpy.types.Curve):
return
if mesh.get("has_layer_styles", None) == True:
return
self.mesh = mesh
self.obj = obj
if element.is_a("IfcTypeProduct"):
self.parse_element_type_material_styles(element)
self.parsed_meshes.add(self.mesh.name)
if not self.ifc_import_settings.load_indexed_maps:
self.load_texture_maps(shape_has_openings)
self.load_texture_maps(shape_has_openings)
self.assign_material_slots_to_faces()
tool.Geometry.record_object_materials(obj)
del self.mesh["ios_materials"]
def load_existing_materials(self) -> None:
for material in bpy.data.materials:
if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
self.styles[ifc_definition_id] = material
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
if self.mesh["ios_materials"]:
return # Already has materials assign to the representation itself
# Otherwise, we need to check for material styles on the element, since
# create_shape on types only works on representations.
mprops = tool.Geometry.get_mesh_props(self.mesh)
context = tool.Ifc.get().by_id(mprops.ifc_definition_id).ContextOfItems
for material in ifcopenshell.util.element.get_materials(element):
if style := ifcopenshell.util.representation.get_material_style(material, context):
self.mesh["ios_materials"] = (style.id(),)
self.mesh["ios_material_ids"] = [0] * len(self.mesh.polygons)
break
def get_ifc_coordinate(self, material: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
"""Get IfcTextureCoordinate"""
texture_style = tool.Style.get_texture_style(material)
@@ -116,7 +94,7 @@ class MaterialCreator:
return coords
# TODO: support IfcTextureMap
if coords.is_a("IfcTextureMap"):
print("WARNING. IfcTextureMap texture coordinates is not supported.")
print(f"WARNING. IfcTextureMap texture coordinates is not supported.")
return
def load_texture_maps(self, shape_has_openings: bool) -> None:
@@ -174,7 +152,7 @@ class MaterialCreator:
if -1 in self.mesh["ios_material_ids"]:
material_to_slot[-1] = get_empty_slot_index()
material_index = np.array([material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]], dtype="I")
material_index = [material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]]
self.mesh.polygons.foreach_set("material_index", material_index)
def resolve_all_stylable_representation_items(
@@ -216,16 +194,14 @@ class IfcImporter:
self.meshes: dict[str, OBJECT_DATA_TYPE] = {}
self.mesh_shapes = {}
self.time = 0
self.unit_scale = 1.0
self.unit_scale = 1
# ifc definition ids to blender elements mapping
self.added_data: dict[int, IFC_CONNECTED_TYPE] = {}
self.native_elements: set[ifcopenshell.entity_instance] = set()
self.native_data: dict[str, Any] = {}
self.native_elements = set()
self.native_data = {}
self.progress = 0
self.material_creator = MaterialCreator(ifc_import_settings, self)
classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe
self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")]
def profile_code(self, message: str) -> None:
if not self.time:
@@ -278,8 +254,6 @@ class IfcImporter:
self.profile_code("Place objects in collections")
self.setup_arrays()
self.profile_code("Setup arrays")
tool.Project.load_linked_models_from_ifc()
self.profile_code("Load linked models")
self.add_project_to_scene()
self.profile_code("Add project to scene")
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
@@ -300,20 +274,6 @@ class IfcImporter:
bpy.context.window_manager.progress_end()
def process_context_filter(self) -> None:
contexts = self.file.by_type("IfcGeometricRepresentationContext")
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
print("Warning! Excessive contexts were found and merged where applicable.")
uniques = {}
i = 0
for element in contexts:
data = "-".join([str(a) for a in element])
if unique := uniques.get(data, None):
ifcopenshell.util.element.replace_element(element, unique)
self.file.remove(element)
i += 1
else:
uniques[data] = element
print(f"Replaced {i} IfcGeometricRepresentationContext")
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
@@ -369,7 +329,7 @@ class IfcImporter:
if self.gross_elements:
print("Warning! Excessive voids were found and skipped for the following elements:")
for element in self.gross_elements:
print(f"{element} - {len(getattr(element, 'HasOpenings', []))} openings")
print(element)
def get_spatial_elements_filtered_by_elements(
self, elements: set[ifcopenshell.entity_instance]
@@ -442,7 +402,7 @@ class IfcImporter:
matrix[2][3] *= self.unit_scale
# Single swept disk solids (e.g. rebar) are better natively represented as beveled curves
if tool.Loader.is_native_swept_disk_solid(element, resolved_representation):
if self.is_native_swept_disk_solid(element, resolved_representation):
self.native_data[element.GlobalId] = {
"matrix": matrix,
"context": context,
@@ -451,10 +411,27 @@ class IfcImporter:
"type": "IfcSweptDiskSolid",
}
return True
def is_native_swept_disk_solid(
self, element: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> bool:
items = [i["item"] for i in ifcopenshell.util.representation.resolve_items(representation)]
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
if tool.Blender.Modifier.is_railing(element):
return False
return True
elif len(items) and ( # See #2508 why we accommodate for invalid IFCs here
items[0].is_a("IfcSweptDiskSolid")
and len({i.is_a() for i in items}) == 1
and len({i.Radius for i in items}) == 1
):
if tool.Blender.Modifier.is_railing(element):
return False
return True
return False
def calculate_model_offset(self) -> None:
props = tool.Georeference.get_georeference_props()
props = bpy.context.scene.BIMGeoreferenceProperties
if self.ifc_import_settings.false_origin_mode == "MANUAL":
tool.Loader.set_manual_blender_offset(self.file)
elif self.ifc_import_settings.false_origin_mode == "AUTOMATIC":
@@ -495,16 +472,14 @@ class IfcImporter:
if grid.WAxes:
self.create_grid_axes(grid.WAxes, grid_obj, grid_placement)
def create_grid_axes(
self, axes: list[ifcopenshell.entity_instance], grid_obj: bpy.types.Object, grid_placement: MatrixType
) -> None:
def create_grid_axes(self, axes, grid_obj, grid_placement):
for axis in axes:
shape = tool.Loader.create_generic_shape(axis.AxisCurve)
mesh = self.create_mesh(axis, shape)
obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh)
obj.show_in_front = True
self.link_element(axis, obj)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement))
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement.copy()))
def create_element_types(self):
for element_type in self.element_types:
@@ -529,9 +504,8 @@ class IfcImporter:
pass
elif shape:
mesh = self.create_mesh(element, shape)
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
break
@@ -559,17 +533,13 @@ class IfcImporter:
native_data = self.native_data[element.GlobalId]
mesh_name = f"{native_data['context'].id()}/{native_data['geometry_id']}"
mesh = self.meshes.get(mesh_name)
curve_thickness = None
if mesh is None:
if native_data["type"] == "IfcSweptDiskSolid":
mesh, curve_thickness = tool.Loader.create_native_swept_disk_solid(element, mesh_name, native_data)
mesh = self.create_native_swept_disk_solid(element, mesh_name, native_data)
tool.Ifc.link(tool.Ifc.get().by_id(native_data["geometry_id"]), mesh)
mesh.name = mesh_name
self.meshes[mesh_name] = mesh
obj = self.create_product(element, mesh=mesh)
tool.Loader.setup_native_swept_disk_solid_thickness(obj, curve_thickness)
self.create_product(element, mesh=mesh)
print("Done creating geometry")
def create_spatial_elements(self) -> None:
@@ -612,7 +582,6 @@ class IfcImporter:
obj.hide_select = True
def create_generic_sqlite_elements(self, elements: set[ifcopenshell.entity_instance]) -> None:
assert isinstance(self.file, ifcopenshell.sql.sqlite)
self.geometry_cache = self.file.get_geometry([e.id() for e in elements])
for geometry_id, geometry in self.geometry_cache["geometry"].items():
mesh_name = tool.Loader.get_mesh_name_from_shape(type("Geometry", (), {"id": geometry_id}))
@@ -622,9 +591,21 @@ class IfcImporter:
mesh["has_cartesian_point_offset"] = False
if geometry["faces"]:
mesh = tool.Loader.create_mesh_from_shape(
mesh=mesh, faces=geometry["faces"].reshape(-1, 3), verts=verts.reshape(-1, 3)
)
num_vertices = len(verts) // 3
total_faces = len(geometry["faces"])
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(geometry["faces"])
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry["faces"])
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.update()
else:
e = geometry["edges"]
v = verts
@@ -717,15 +698,24 @@ class IfcImporter:
def create_structural_point_connections(self):
for product in self.file.by_type("IfcStructuralPointConnection"):
# TODO: make this based off ifcopenshell. See #1409
representation: ifcopenshell.entity_instance = next(
rep for rep in product.Representation.Representations if rep.RepresentationType == "Vertex"
)
mesh = tool.Loader.create_structural_point_connection_mesh(representation)
if mesh is None:
continue
tool.Ifc.link(representation, mesh)
placement_matrix = ifcopenshell.util.placement.get_local_placement(product.ObjectPlacement)
vertex = None
context = None
representation = None
for subelement in self.file.traverse(product.Representation):
if subelement.is_a("IfcVertex") and subelement.VertexGeometry.is_a("IfcCartesianPoint"):
vertex = list(subelement.VertexGeometry.Coordinates)
elif subelement.is_a("IfcGeometricRepresentationContext"):
context = subelement
elif subelement.is_a("IfcTopologyRepresentation"):
representation = subelement
if not vertex or not context or not representation:
continue # TODO implement non cartesian point vertexes
mesh_name = tool.Geometry.get_representation_name(representation)
mesh = bpy.data.meshes.new(mesh_name)
mesh.from_pydata([mathutils.Vector(vertex) * self.unit_scale], [], [])
obj = bpy.data.objects.new(tool.Loader.get_name(product), mesh)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, placement_matrix))
self.link_element(product, obj)
@@ -761,9 +751,12 @@ class IfcImporter:
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
result = set()
for product in products:
if representation := self.get_pointcloud_representation(product):
if pointcloud := self.create_pointcloud(product, representation):
representation = self.get_pointcloud_representation(product)
if representation is not None:
pointcloud = self.create_pointcloud(product, representation)
if pointcloud is not None:
result.add(pointcloud)
return result
def create_pointcloud(
@@ -811,23 +804,21 @@ class IfcImporter:
materials_updated = bool(mesh)
if mesh is None:
mesh = self.create_mesh(element, shape)
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
mesh = None
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
self.link_element(element, obj)
if getattr(element, "HasOpenings", None):
tool.Geometry.lock_scale(obj)
for ifcclass in self.classes_to_wireframe_list:
if element.is_a(ifcclass):
obj.display_type = "WIRE"
if shape and mesh:
if shape:
# We use numpy here because Blender mathutils.Matrix is not accurate enough
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat))
assert mesh # Type checker.
if not materials_updated:
self.material_creator.create(element, obj, mesh, tool.Geometry.does_shape_has_openings(shape))
elif mesh: # When does this occur?
@@ -845,6 +836,43 @@ class IfcImporter:
def load_existing_meshes(self) -> None:
self.meshes.update({m.name: m for m in bpy.data.meshes})
def create_native_swept_disk_solid(
self, element: ifcopenshell.entity_instance, mesh_name: str, native_data: dict[str, Any]
) -> bpy.types.Curve:
# TODO: georeferencing?
curve = bpy.data.curves.new(mesh_name, type="CURVE")
curve.dimensions = "3D"
curve.resolution_u = 2
polyline = curve.splines.new("POLY")
for item_data in ifcopenshell.util.representation.resolve_items(native_data["representation"]):
item = item_data["item"]
matrix = item_data["matrix"]
matrix[0][3] *= self.unit_scale
matrix[1][3] *= self.unit_scale
matrix[2][3] *= self.unit_scale
# TODO: support inner radius, start param, and end param
geometry = tool.Loader.create_generic_shape(item.Directrix)
if not geometry:
continue
e = geometry.edges
v = geometry.verts
vertices = [list(matrix @ [v[i], v[i + 1], v[i + 2], 1]) for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
v2 = None
for edge in edges:
v1 = vertices[edge[0]]
if v1 != v2:
polyline = curve.splines.new("POLY")
polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v1)
v2 = vertices[edge[1]]
polyline.points.add(1)
polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v2)
curve.bevel_depth = self.unit_scale * item.Radius
curve.use_fill_caps = True
return curve
def merge_materials_by_colour(self):
cleaned_materials = {}
for m in bpy.data.materials:
@@ -898,20 +926,18 @@ class IfcImporter:
def load_file(self):
self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file)
props = tool.Blender.get_bim_props()
if not props.ifc_file:
props.ifc_file = self.ifc_import_settings.input_file
self.file = tool.Ifc.get()
if not bpy.context.scene.BIMProperties.ifc_file:
bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file
self.file = IfcStore.get_file()
def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
tool.Loader.set_unit_scale(self.unit_scale)
def set_units(self) -> None:
if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext):
def set_units(self):
if not (units := self.file.by_type("IfcUnitAssignment")):
return # Geometry is optional in IFC
props = tool.Blender.get_bim_props()
for unit in assignment.Units:
for unit in units[0].Units:
if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT":
if unit.is_a("IfcSIUnit"):
bpy.context.scene.unit_settings.system = "METRIC"
@@ -930,27 +956,26 @@ class IfcImporter:
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
try:
props.area_unit = "{}{}".format(
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
props.area_unit = "SQUARE_METRE"
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
try:
props.volume_unit = "{}{}".format(
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
props.volume_unit = "CUBIC_METRE"
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
def create_project(self):
project = self.file.by_type("IfcProject")[0]
self.project = {"ifc": project}
obj = tool.Ifc.get_object(project)
if obj:
props = tool.Blender.get_object_bim_props(obj)
self.project["blender"] = props.collection
self.project["blender"] = obj.BIMObjectProperties.collection
self.has_existing_project = True
return
self.project["blender"] = bpy.data.collections.new(
@@ -960,16 +985,10 @@ class IfcImporter:
obj.hide_select = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
props = tool.Blender.get_object_bim_props(obj)
props.collection = self.collections[project.GlobalId] = self.project["blender"]
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
def create_styles(self) -> None:
styles = self.file.by_type("IfcSurfaceStyle")
if len(styles) > self.ifc_import_settings.style_limit: # Probably something strange happening
print("Warning! Excessive styles were found and merged where applicable.")
ifcpatch.execute({"file": self.file, "recipe": "MergeStyles", "arguments": []})
styles = self.file.by_type("IfcSurfaceStyle")
for style in styles:
for style in self.file.by_type("IfcSurfaceStyle"):
self.create_style(style)
def create_style(self, style: ifcopenshell.entity_instance) -> None:
@@ -982,11 +1001,10 @@ class IfcImporter:
self.material_creator.styles[style.id()] = blender_material
style_elements = tool.Style.get_style_elements(blender_material)
props = tool.Style.get_material_style_props(blender_material)
if tool.Style.has_blender_external_style(style_elements):
props.active_style_type = "External"
blender_material.BIMStyleProperties.active_style_type = "External"
else:
props.active_style_type = "Shading"
blender_material.BIMStyleProperties.active_style_type = "Shading"
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
@@ -994,10 +1012,10 @@ class IfcImporter:
tool.Collector.assign(obj, should_clean_users_collection=False)
def is_curve_annotation(self, element: ifcopenshell.entity_instance) -> bool:
object_type = ifcopenshell.util.element.get_predefined_type(element)
object_type = element.ObjectType
return (
object_type in tool.Drawing.ANNOTATION_TYPES_DATA
and tool.Drawing.ANNOTATION_TYPES_DATA[object_type].data_type == "curve"
and tool.Drawing.ANNOTATION_TYPES_DATA[object_type][3] == "curve"
)
def get_drawing_group(self, element):
@@ -1042,7 +1060,11 @@ class IfcImporter:
v2 = vertices[edge[1]]
polyline.points.add(1)
polyline.points[-1].co = mathutils.Vector(v2)
edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist()
# TODO: remove error handling after we update build in Bonsai.
try:
edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist()
except AttributeError:
edges_item_ids = []
curve["ios_edges_item_ids"] = edges_item_ids
return curve
@@ -1050,8 +1072,8 @@ class IfcImporter:
self,
element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> Union[bpy.types.Mesh, None]:
cartesian_point_offset=None,
) -> bpy.types.Mesh:
try:
if hasattr(shape, "geometry"):
# shape is ShapeElementType
@@ -1066,36 +1088,35 @@ class IfcImporter:
old_mesh.name = mesh_name + ".old"
mesh = bpy.data.meshes.new(mesh_name)
verts = ifcopenshell.util.shape.get_vertices(geometry)
if cartesian_point_offset is False:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
elif cartesian_point_offset is not None:
verts = verts - cartesian_point_offset
verts_array = np.array(geometry.verts)
offset = np.array([-cartesian_point_offset[0], -cartesian_point_offset[1], -cartesian_point_offset[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = (
f"{cartesian_point_offset[0]},{cartesian_point_offset[1]},{cartesian_point_offset[2]}"
)
elif verts.size and tool.Loader.is_point_far_away(verts[0], is_meters=True):
elif geometry.verts and tool.Loader.is_point_far_away(
(geometry.verts[0], geometry.verts[1], geometry.verts[2]), is_meters=True
):
# Shift geometry close to the origin based off that first vert it found
offset = verts[0]
verts = verts - offset
verts_array = np.array(geometry.verts)
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = f"{offset[0]},{offset[1]},{offset[2]}"
mesh["cartesian_point_offset"] = f"{geometry.verts[0]},{geometry.verts[1]},{geometry.verts[2]}"
else:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
mesh = tool.Loader.convert_geometry_to_mesh(
geometry,
mesh,
verts=verts,
load_indexed_maps=self.ifc_import_settings.load_indexed_maps,
)
# E.g. `create_mesh` is also used for IfcRelSpaceBoundary.
if element.is_a("IfcObjectDefinition"):
return tool.Loader.slice_layerset_mesh(element, mesh)
return mesh
return tool.Loader.convert_geometry_to_mesh(geometry, mesh, verts=verts)
except:
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
import traceback
@@ -1103,10 +1124,9 @@ class IfcImporter:
print(traceback.format_exc())
def set_default_context(self):
rprops = tool.Root.get_root_props()
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
if subcontext.ContextIdentifier == "Body":
rprops.contexts = str(subcontext.id())
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
break
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
@@ -1152,7 +1172,6 @@ class IfcImportSettings:
self.deflection_tolerance = 0.001
self.angular_tolerance = 0.5
self.void_limit = 30
self.style_limit = 300
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
self.distance_limit = 1000
@@ -1165,16 +1184,15 @@ class IfcImportSettings:
self.has_filter = None
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.contexts: list[ifcopenshell.entity_instance] = []
self.contexts = []
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.elements: set[ifcopenshell.entity_instance] = set()
self.load_indexed_maps = False
@staticmethod
def factory(context=None, input_file=None, logger=None):
scene_diff = tool.Blender.get_diff_props()
props = tool.Project.get_project_props()
scene_diff = bpy.context.scene.DiffProperties
props = bpy.context.scene.BIMProjectProperties
settings = IfcImportSettings()
settings.input_file = input_file
if logger is None:
@@ -1190,27 +1208,17 @@ class IfcImportSettings:
settings.deflection_tolerance = props.deflection_tolerance
settings.angular_tolerance = props.angular_tolerance
settings.void_limit = props.void_limit
settings.style_limit = props.style_limit
settings.distance_limit = props.distance_limit
settings.false_origin_mode = props.false_origin_mode
try:
settings.false_origin = [float(o) for o in props.false_origin.split(",")[:3]]
except Exception as e:
print(traceback.format_exc())
raise Exception(
f"Failed to set false origin from string '{props.false_origin}'.\n"
f"Error: {str(e)}.\nSee above for the details."
)
except:
settings.false_origin = [0, 0, 0]
try:
settings.project_north = float(props.project_north)
except Exception as e:
print(traceback.format_exc())
raise Exception(
f"Failed to set project north from string '{props.project_north}'.\n"
f"Error: {str(e)}.\nSee above for the details."
)
except:
settings.project_north = 0
settings.element_limit_mode = props.element_limit_mode
settings.element_offset = props.element_offset
settings.element_limit = props.element_limit
settings.load_indexed_maps = props.load_indexed_maps
return settings
@@ -30,11 +30,7 @@ classes = (
operator.BIM_OT_aggregate_unassign_object,
operator.BIM_OT_break_link_to_other_aggregates,
operator.BIM_OT_select_linked_aggregates,
operator.BIM_OT_disable_aggregate_mode,
operator.BIM_OT_toggle_aggregate_mode_local_view,
prop.BIMObjectAggregateProperties,
prop.Objects,
prop.BIMAggregateProperties,
ui.BIM_PT_aggregate,
ui.BIM_PT_linked_aggregate,
)
@@ -42,9 +38,7 @@ classes = (
def register():
bpy.types.Object.BIMObjectAggregateProperties = bpy.props.PointerProperty(type=prop.BIMObjectAggregateProperties)
bpy.types.Scene.BIMAggregateProperties = bpy.props.PointerProperty(type=prop.BIMAggregateProperties)
def unregister():
del bpy.types.Object.BIMObjectAggregateProperties
del bpy.types.Scene.BIMAggregateProperties
@@ -1,340 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2024 Bruno Perdigão <contact@brunopo.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import blf
import bpy
import gpu
import ifcopenshell
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
from bonsai.bim.module.geometry.decorator import ItemDecorator
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
def create_bounding_box(objs):
# Initialize the bounding box coordinates
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
max_x, max_y, max_z = float("-inf"), float("-inf"), float("-inf")
# Iterate over the selected objects
for obj in objs:
if not obj.data:
continue
# Get the object's bounding box coordinates
bbox_world = [obj.matrix_world @ Vector(b) for b in obj.bound_box]
obj_min = []
obj_min.append(min([b[0] for b in bbox_world]))
obj_min.append(min([b[1] for b in bbox_world]))
obj_min.append(min([b[2] for b in bbox_world]))
obj_max = []
obj_max.append(max([b[0] for b in bbox_world]))
obj_max.append(max([b[1] for b in bbox_world]))
obj_max.append(max([b[2] for b in bbox_world]))
# Update the overall bounding box coordinates
min_x = min(min_x, min(obj_min[0], obj_max[0]))
min_y = min(min_y, min(obj_min[1], obj_max[1]))
min_z = min(min_z, min(obj_min[2], obj_max[2]))
max_x = max(max_x, max(obj_min[0], obj_max[0]))
max_y = max(max_y, max(obj_min[1], obj_max[1]))
max_z = max(max_z, max(obj_min[2], obj_max[2]))
indices = [
(min_x, min_y, min_z),
(max_x, min_y, min_z),
(max_x, max_y, min_z),
(min_x, max_y, min_z),
(min_x, min_y, max_z),
(max_x, min_y, max_z),
(max_x, max_y, max_z),
(min_x, max_y, max_z),
]
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)]
return indices, edges
class AggregateDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_aggregate, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "u_ViewProjectionMatrix")
shader_info.push_constant("FLOAT", "u_Scale")
shader_info.vertex_in(0, "VEC3", "position")
shader_info.vertex_in(1, "FLOAT", "arcLength")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, "VEC4", "FragColor")
shader_info.push_constant("VEC4", "color")
shader_info.vertex_source(
"void main()"
"{"
" v_ArcLength = arcLength;"
" gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f);"
"}"
)
shader_info.fragment_source(
"void main()" "{" " if (step(sin(v_ArcLength * u_Scale), 0.4) == 1) discard;" " FragColor = color;" "}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
return shader
def draw_custom_batch(self, coords, color):
indices = None
if not tool.Blender.validate_shader_batch_data(coords, indices):
return
shader = self.dotted_line_shader()
arc_lengths = [0]
for a, b in zip(coords[:-1], coords[1:]):
arc_lengths.append(arc_lengths[-1] + (a - b).length)
batch = batch_for_shader(
shader,
"LINE_STRIP",
{"position": coords, "arcLength": arc_lengths},
indices=indices,
)
matrix = bpy.context.region_data.perspective_matrix
shader.uniform_float("color", color)
shader.uniform_float("u_ViewProjectionMatrix", matrix)
shader.uniform_float("u_Scale", 25)
batch.draw(shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_aggregate(self, context):
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
decorator_color_error = self.addon_prefs.decorator_color_error
decorator_color_unselected = self.addon_prefs.decorator_color_unselected
decorator_color_background = self.addon_prefs.decorator_color_background
theme = context.preferences.themes.items()[0][1]
selected_object_color = (*theme.view_3d.object_active, 1)
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
aggregates = []
if not (selected_objects := context.selected_objects):
return
for obj in selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
return
parts = ifcopenshell.util.element.get_parts(element)
if parts:
aggregates.append(obj)
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregates.append(tool.Ifc.get_object(aggregate))
aggregates = set(aggregates)
for aggregate in aggregates:
self.line_shader.uniform_float("lineWidth", 1.0)
color = decorator_color_unselected
if aggregate in selected_objects:
color = selected_object_color
size = aggregate.empty_display_size
location = aggregate.location
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
self.line_shader.uniform_float("lineWidth", 0.5)
self.draw_batch("LINES", indices, color, edges)
line = (Vector(indices[0]), location)
self.draw_custom_batch(line, decorator_color_unselected)
class AggregateModeDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_aggregate_name, (context,), "WINDOW", "POST_PIXEL")
)
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
)
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_aggregate_name(self, context):
if context.mode == "EDIT_MESH":
return
region = context.region
rv3d = region.data
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
font_size = tool.Blender.scale_font_size(12)
blf.size(self.font_id, font_size)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorator_color_selected
if aggregate_obj in context.selected_objects:
color = self.addon_prefs.decorator_color_selected
blf.color(self.font_id, *color)
text = aggregate_obj.name
text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, aggregate_obj.location)
if not text_coords:
return
text_length = blf.dimensions(self.font_id, text)
text_coords[0] -= text_length[0] / 2
text_coords[1] -= 20
blf.position(self.font_id, text_coords[0], text_coords[1], 0)
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
blf.draw(self.font_id, text)
def draw_aggregate_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
theme = context.preferences.themes.items()[0][1]
selected_object_color = (*theme.view_3d.object_active, 1)
color = self.addon_prefs.decorator_color_selected
if aggregate_obj in context.selected_objects:
color = selected_object_color
size = aggregate_obj.empty_display_size
location = aggregate_obj.location
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
if parts:
for part in parts:
part_obj = tool.Ifc.get_object(part)
if part.is_a("IfcElementAssembly"):
self.line_shader.uniform_float("lineWidth", 1.0)
size = part_obj.empty_display_size
location = part_obj.location
color = self.addon_prefs.decorator_color_unselected
if part_obj in context.selected_objects:
color = selected_object_color
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
color = self.addon_prefs.decorator_color_selected
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
self.line_shader.uniform_float("lineWidth", 0.5)
self.draw_batch("LINES", indices, color, edges)
@@ -66,10 +66,6 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
relating_obj=relating_obj,
related_obj=obj,
)
props = context.scene.BIMAggregateProperties
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
new_editing_obj = props.editing_objects.add()
new_editing_obj.obj = obj
except core.IncompatibleAggregateError:
self.report({"ERROR"}, f"Cannot aggregate {obj.name} to {relating_obj.name}")
except core.AggregateRepresentationError:
@@ -369,33 +365,3 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
obj.select_set(True)
return {"FINISHED"}
class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
bl_idname = "bim.disable_aggregate_mode"
bl_label = "Disable Aggregate Mode"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
bl_idname = "bim.toggle_aggregate_mode_local_view"
bl_label = "Toggle Aggregate Mode Local View"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMAggregateProperties
objs = [o.obj for o in props.editing_objects]
if props.in_aggregate_mode:
if context.space_data.local_view:
bpy.ops.view3d.localview()
else:
for obj in objs:
obj.select_set(True)
bpy.ops.view3d.localview()
return {"FINISHED"}
@@ -32,7 +32,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from bonsai.bim.module.aggregate.decorator import AggregateDecorator, AggregateModeDecorator
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
@@ -74,20 +73,6 @@ def poll_related_object(self: "BIMObjectAggregateProperties", related_obj: bpy.t
return True
def update_aggregate_decorator(self, context):
if self.aggregate_decorator:
AggregateDecorator.install(bpy.context)
else:
AggregateDecorator.uninstall()
def update_aggregate_mode_decorator(self, context):
if self.in_aggregate_mode:
AggregateModeDecorator.install(bpy.context)
else:
AggregateModeDecorator.uninstall()
class BIMObjectAggregateProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object)
@@ -97,21 +82,3 @@ class BIMObjectAggregateProperties(PropertyGroup):
type=bpy.types.Object,
poll=poll_related_object,
)
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
previous_hide_select: bpy.props.BoolProperty(default=False)
class BIMAggregateProperties(PropertyGroup):
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
editing_objects: CollectionProperty(type=Objects)
not_editing_objects: CollectionProperty(type=Objects)
aggregate_decorator: BoolProperty(
name="Display Aggregate",
default=False,
update=update_aggregate_decorator,
)
+9 -12
View File
@@ -34,12 +34,12 @@ class BIM_PT_aggregate(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
if not IfcStore.get_element(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
@@ -47,9 +47,6 @@ class BIM_PT_aggregate(Panel):
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text="Aggregate Decorator")
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
if not AggregateData.is_loaded:
AggregateData.load()
@@ -66,9 +63,9 @@ class BIM_PT_aggregate(Panel):
col.enabled = False
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
if props.relating_object:
op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
elif props.related_object:
op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
return
else:
@@ -115,14 +112,14 @@ class BIM_PT_linked_aggregate(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
if not IfcStore.get_element(props.ifc_definition_id):
return False
if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
return True
@@ -1,36 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
# from . import ui, prop, operator
from . import operator
classes = (operator.ImportAlignmentCSV,)
def menu_func_import(self, context):
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
def register():
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
@@ -1,113 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
# pyright: reportUnnecessaryTypeIgnoreComment=error
import os
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.add_stationing_to_alignment
import bpy
import json
import time
import calendar
import isodate
import bonsai.core.sequence as core
import bonsai.tool as tool
import bonsai.bim.module.sequence.helper as helper
import ifcopenshell.api.spatial
import ifcopenshell.geom
import ifcopenshell.util.sequence
import ifcopenshell.util.selector
from datetime import datetime
from dateutil import parser, relativedelta
from bpy_extras.io_utils import ImportHelper
from typing import get_args, TYPE_CHECKING
from typing_extensions import assert_never
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_alignment_csv"
bl_label = "Import Alignment CSV"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@classmethod
def poll(cls, context):
ifc_file = tool.Ifc.get()
if ifc_file is None:
cls.poll_message_set("No IFC file is loaded.")
return False
elif ifc_file.schema != "IFC4X3":
cls.poll_message_set("Schema must be IFC4x3.")
return False
return True
def _execute(self, context):
self.file = tool.Ifc.get()
start = time.time()
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
sites = self.file.by_type("IfcSite")
for site in sites:
ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site)
# process the generated IfcReferent for the alignment
for rel in alignment.IsNestedBy:
for referent in rel.RelatedObjects:
if referent.is_a("IfcReferent"):
referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None)
tool.Geometry.link(referent, referent_obj)
tool.Collector.assign(referent_obj, should_clean_users_collection=False)
# an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal)
# get all the alignment curves
curves = []
for rel in alignment.IsDecomposedBy:
for agg in rel.RelatedObjects:
if agg.is_a("IfcAlignment"):
curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve
# if there aren't any curves from aggregation, then there is only a single vertical or no vertical
if len(curves) == 0:
curves.append(ifcopenshell.api.alignment.get_curve(alignment))
settings = ifcopenshell.geom.settings()
for curve in curves:
shape = ifcopenshell.geom.create_shape(settings, curve)
# create a new Blender mesh
mesh_name = tool.Loader.get_mesh_name_from_shape(shape)
mesh = bpy.data.meshes.new(mesh_name)
m = tool.Loader.convert_geometry_to_mesh(shape, mesh)
# create a new Blender object
alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m)
# link the blender object to with the alignment element
tool.Geometry.link(alignment, alignment_obj)
# assign the object to the blender collections
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
@@ -38,16 +38,13 @@ class AttributesData:
def attributes(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
assert element
data = element.get_info()
if "GlobalId" in data:
if hasattr(element, "GlobalId"):
excluded_keys = ["id", "type"]
else:
excluded_keys = ["type"]
# Same types also filtered by `import_attribute`.
exclude_value_types = (tuple, ifcopenshell.entity_instance)
for key, value in data.items():
if value is None or isinstance(value, exclude_value_types) or key in excluded_keys:
if value is None or isinstance(value, ifcopenshell.entity_instance) or key in excluded_keys:
continue
if key == "id":
key = "STEP ID"
@@ -19,42 +19,28 @@
import bpy
import json
import ifcopenshell
import ifcopenshell.api.attribute
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
def get_objs_for_operation(operator_properties, context):
if operator_properties.obj:
return [bpy.data.objects[operator_properties.obj]]
if operator_properties.mass_operation:
return context.selected_objects[:]
return [context.active_object]
from bonsai.bim.ifc import IfcStore
class EnableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
obj: bpy.props.StringProperty()
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def enable_editing_attribute_on_obj(self, obj):
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects[self.obj]
props = obj.BIMAttributeProperties
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
if not element:
return
has_inherited_predefined_type = False
if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)):
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
@@ -80,65 +66,52 @@ class EnableEditingAttributes(bpy.types.Operator):
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
props.is_editing_attributes = True
def execute(self, context):
for obj in get_objs_for_operation(self, context):
self.enable_editing_attribute_on_obj(obj)
return {"FINISHED"}
class DisableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes"
bl_description = "ALT + Left Click to disable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def disable_editing_attributes_on_obj(self, obj):
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
obj: bpy.props.StringProperty()
def execute(self, context):
for obj in get_objs_for_operation(self, context):
self.disable_editing_attributes_on_obj(obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
return {"FINISHED"}
class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_attributes"
bl_label = "Edit Attributes"
bl_description = "Edit the attributes of the active object"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def _execute(self, context):
self.file = tool.Ifc.get()
obj = tool.Blender.get_active_object(is_selected=False)
if not (element := tool.Ifc.get_entity(obj)):
return
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
def callback(attributes, prop):
if prop.name in ("RefLatitude", "RefLongitude"):
if not prop.is_null:
if prop.is_null:
attributes[prop.name] = None
else:
try:
attributes[prop.name] = json.loads(prop.string_value)
except:
attributes[prop.name] = None
return True
return True
props = obj.BIMAttributeProperties
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
tool.Root.set_object_name(obj, element)
if (name := tool.Loader.get_name(product)) and obj.name != name:
obj.name = name
bpy.ops.bim.disable_editing_attributes(obj=obj.name)
if tool.Root.is_spatial_element(element):
bonsai.core.spatial.import_spatial_decomposition(tool.Spatial)
return {"FINISHED"}
class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
@@ -187,12 +160,9 @@ class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.copy_attribute_to_selection"
bl_label = "Copy Attribute To Selection"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
def _execute(self, context):
value = tool.Blender.get_active_object().BIMAttributeProperties.attributes.get(self.name).get_value()
total = core.copy_attribute_to_selection(
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
)
self.report({"INFO"}, f"Attribute was successfully copied to {total} elements.")
value = context.active_object.BIMAttributeProperties.attributes.get(self.name).get_value()
for obj in tool.Blender.get_selected_objects():
core.copy_attribute_to_selection(tool.Ifc, name=self.name, value=value, obj=obj)
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
+14 -6
View File
@@ -18,29 +18,33 @@
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
def draw_ui(context, layout, attributes):
obj = context.active_object
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
if props.is_editing_attributes:
row = layout.row(align=True)
row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
op.obj = obj.name
op = row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
op.obj = obj.name
bonsai.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
else:
row = layout.row()
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
op.obj = obj.name
for attribute in attributes:
row = layout.row(align=True)
row.label(text=attribute["name"])
value = bonsai.bim.helper.get_display_value(attribute["value"])
op = row.operator("bim.select_similar", text=value, icon="NONE", emboss=False)
# row.label(text=attribute["value"])
op = row.operator("bim.select_similar", text=attribute["value"], icon="NONE", emboss=False)
op.key = attribute["name"]
# TODO: reimplement, see #1222
@@ -58,7 +62,11 @@ class BIM_PT_object_attributes(Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get_entity(context.active_object)
if not context.active_object:
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
def draw(self, context):
if not AttributesData.is_loaded:
@@ -113,7 +113,7 @@ class AuginCreateNewModel(bpy.types.Operator):
context.scene.collection.objects.link(cam_obj)
context.scene.camera = cam_obj
tmpdir = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None)
tmpdir = tempfile.mkdtemp()
thumb_path = os.path.join(tmpdir, "thumb.png")
context.scene.render.image_settings.file_format = "PNG"
context.scene.render.filepath = thumb_path
@@ -122,16 +122,15 @@ class AuginCreateNewModel(bpy.types.Operator):
context.scene.render.image_settings.file_format = old_file_format
context.scene.render.filepath = old_filepath
bim_props = tool.Blender.get_bim_props()
client.upload_file(bim_props.ifc_file, result["s3_bucket"], result["model_path"])
client.upload_file(context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"])
client.upload_file(thumb_path, result["s3_bucket"], result["thumb_path"])
# Notify done
url = "https://server.auge.pro.br/API/v3/augin_rest.php/files_uploaded"
payload = {
"user_token": props.token,
"ifc_filesize": os.path.getsize(bim_props.ifc_file),
"model_filesize": os.path.getsize(bim_props.ifc_file),
"ifc_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
"model_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
"thumb_filesize": os.path.getsize(thumb_path),
"model_upload_path": result["model_path"],
"thumb_upload_path": result["thumb_path"],
+1 -3
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy.types
import bonsai.tool as tool
class BIM_PT_augin(bpy.types.Panel):
@@ -48,8 +47,7 @@ class BIM_PT_augin(bpy.types.Panel):
row = layout.row()
row.label(text="Logged in as " + props.username)
bim_props = tool.Blender.get_bim_props()
if not bim_props.ifc_file:
if not context.scene.BIMProperties.ifc_file:
row = layout.row()
row.label(text="No IFC Found")
return
+2 -4
View File
@@ -21,7 +21,6 @@ import bpy
import bcf
import bcf.bcfxml
import bcf.v2.bcfxml
import bonsai.tool as tool
from typing import Union
@@ -31,8 +30,7 @@ class BcfStore:
@classmethod
def get_bcfxml(cls) -> Union[bcf.bcfxml.BcfXml, None]:
if not cls.bcfxml:
props = tool.Bcf.get_bcf_props()
bcf_filepath = props.bcf_file
bcf_filepath = bpy.context.scene.BCFProperties.bcf_file
if not os.path.isabs(bcf_filepath):
bcf_filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), bcf_filepath))
if bcf_filepath:
@@ -48,7 +46,7 @@ class BcfStore:
@classmethod
def set(cls, bcfxml: Union[bcf.bcfxml.BcfXml, None], filepath: str) -> None:
cls.bcfxml = bcfxml
props = tool.Bcf.get_bcf_props()
props = bpy.context.scene.BCFProperties
props.bcf_file = filepath
# Set bcf_version prop on load.
+94 -96
View File
@@ -42,8 +42,8 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.bcf.prop as bcf_prop
import bonsai.bim.module.bcf.bcfstore as bcfstore
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai.bim.ifc import IfcStore
from math import radians, degrees, atan, tan, cos, sin
from mathutils import Vector, Matrix, Euler, geometry
from xsdata.models.datatype import XmlDateTime
@@ -55,7 +55,7 @@ class NewBcfProject(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
bcf_v2 = props.bcf_version == "2"
bcf_class = bcf.v2.bcfxml.BcfXml if bcf_v2 else bcf.v3.bcfxml.BcfXml
bcfxml = bcf_class.create_new("New Project")
@@ -64,14 +64,12 @@ class NewBcfProject(bpy.types.Operator):
return {"FINISHED"}
class LoadBcfProject(bpy.types.Operator, ImportHelper):
class LoadBcfProject(bpy.types.Operator):
bl_idname = "bim.load_bcf_project"
bl_label = "Load BCF Project"
bl_description = "Load the BCF file."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
filename_ext = ".bcf"
def execute(self, context):
# Operator is also used when new project is created by not yet saved.
@@ -105,12 +103,15 @@ class LoadBcfProject(bpy.types.Operator, ImportHelper):
assert bcfxml.project
if bcfxml.project.name is None:
bcfxml.project.name = nameless
props = tool.Bcf.get_bcf_props()
props.name = bcfxml.project.name
context.scene.BCFProperties.name = bcfxml.project.name
bpy.ops.bim.load_bcf_topics()
self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.")
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class UnloadBcfProject(bpy.types.Operator):
bl_idname = "bim.unload_bcf_project"
@@ -130,7 +131,7 @@ class LoadBcfTopics(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
props.topics.clear()
# workaround, one non standard topic would break reading entire bcf
# ignored these topics ATM
@@ -162,8 +163,7 @@ class LoadBcfTopic(bpy.types.Operator):
assert bcfxml
topic = bcfxml.topics[self.topic_guid]
bcfxml.get_header(self.topic_guid)
props = tool.Bcf.get_bcf_props()
new = props.topics[self.topic_index]
new = context.scene.BCFProperties.topics[self.topic_index]
data_map = {
"name": topic.guid,
"title": topic.topic.title,
@@ -244,8 +244,7 @@ class LoadBcfComments(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
blender_topic = props.topics.get(self.topic_guid)
blender_topic = context.scene.BCFProperties.topics.get(self.topic_guid)
blender_topic.comments.clear()
for comment in bcfxml.topics[self.topic_guid].comments:
new = blender_topic.comments.add()
@@ -275,9 +274,7 @@ class EditBcfProjectName(bpy.types.Operator):
# Bonsai creates default project on load.
assert bcfxml.project
props = tool.Bcf.get_bcf_props()
bcfxml.project.name = props.name
bcfxml.project.name = context.scene.BCFProperties.name
return {"FINISHED"}
@@ -287,7 +284,7 @@ class EditBcfTopicName(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
@@ -303,7 +300,7 @@ class EditBcfTopic(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
@@ -340,15 +337,13 @@ class EditBcfTopic(bpy.types.Operator):
return {"FINISHED"}
class SaveBcfProject(bpy.types.Operator, ExportHelper):
class SaveBcfProject(bpy.types.Operator):
bl_idname = "bim.save_bcf_project"
bl_label = "Save BCF Project"
bl_description = "Save active BCF project by the provided filepath."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
filename_ext = ".bcf"
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
@@ -365,7 +360,8 @@ class SaveBcfProject(bpy.types.Operator, ExportHelper):
self.filepath = str(path)
return self.execute(context)
return ExportHelper.invoke(self, context, event)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class AddBcfTopic(bpy.types.Operator):
@@ -375,15 +371,13 @@ class AddBcfTopic(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
return props.author
return context.scene.BCFProperties.author
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
bcfxml.add_topic("New Topic", "", props.author)
bcfxml.add_topic("New Topic", "", context.scene.BCFProperties.author)
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -395,7 +389,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
props_are_filled = all(
(getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type"))
)
@@ -409,7 +403,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
is_external = "://" in props.bim_snippet_reference
@@ -441,7 +435,7 @@ class AddBcfRelatedTopic(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
related_topics = tool.Bcf.get_topic_related_topics(topic)
@@ -468,15 +462,14 @@ class AddBcfHeaderFile(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
return props.file_reference
return context.scene.BCFProperties.file_reference
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -526,10 +519,9 @@ class ViewBcfTopic(bpy.types.Operator):
topic_guid: bpy.props.StringProperty()
def execute(self, context):
props = tool.Bcf.get_bcf_props()
for index, topic in enumerate(props.topics):
for index, topic in enumerate(context.scene.BCFProperties.topics):
if topic.name.lower() == self.topic_guid.lower():
props.active_topic_index = index
context.scene.BCFProperties.active_topic_index = index
break
return {"FINISHED"}
@@ -554,7 +546,7 @@ class AddBcfViewpoint(bpy.types.Operator):
blender_camera = context.scene.camera
assert blender_camera
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -624,7 +616,7 @@ class AddBcfViewpoint(bpy.types.Operator):
old_file_format = blender_render.image_settings.file_format
blender_render.image_settings.file_format = "PNG"
old_filepath = blender_render.filepath
blender_render.filepath = tool.Blender.get_data_dir_path("snapshot.png").__str__()
blender_render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
bpy.ops.render.opengl(write_still=True)
with open(blender_render.filepath, "rb") as f:
snapshot = f.read()
@@ -683,7 +675,7 @@ class RemoveBcfViewpoint(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
if not bcfxml:
return False
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
topic = props.active_topic
if not topic:
return False
@@ -698,7 +690,7 @@ class RemoveBcfViewpoint(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
del topic.viewpoints[blender_topic.viewpoints]
@@ -723,7 +715,7 @@ class RemoveBcfFile(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
header_files = tool.Bcf.get_topic_header_files(topic)
@@ -741,14 +733,13 @@ class RemoveBcfTopic(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
return props.topics
return context.scene.BCFProperties.topics
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
del bcfxml.topics[props.active_topic.name]
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -761,14 +752,13 @@ class AddBcfReferenceLink(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
return bool(props.reference_link)
return context.scene.BCFProperties.reference_link
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = tool.Bcf.get_topic_reference_links(topic)
@@ -786,15 +776,14 @@ class AddBcfDocumentReference(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
return bool(props.document_reference)
return context.scene.BCFProperties.document_reference
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -866,14 +855,13 @@ class AddBcfLabel(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
return bool(props.label)
return context.scene.BCFProperties.label
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
new = blender_topic.labels.add()
@@ -895,7 +883,7 @@ class EditBcfReferenceLinks(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = [r.name for r in blender_topic.reference_links]
@@ -912,7 +900,7 @@ class EditBcfLabels(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
labels = [l.name for l in blender_topic.labels]
@@ -930,7 +918,7 @@ class RemoveBcfReferenceLink(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = tool.Bcf.get_topic_reference_links(topic)
@@ -950,7 +938,7 @@ class RemoveBcfLabel(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
labels = tool.Bcf.get_topic_labels(topic)
@@ -969,7 +957,7 @@ class RemoveBcfBimSnippet(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
tool.Bcf.set_topic_bim_snippet(topic, None)
@@ -990,7 +978,7 @@ class RemoveBcfDocumentReference(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -1055,7 +1043,7 @@ class RemoveBcfRelatedTopic(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
related_topics = tool.Bcf.get_topic_related_topics(topic)
@@ -1075,7 +1063,7 @@ class RemoveBcfComment(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
comments = topic.comments
@@ -1096,7 +1084,7 @@ class EditBcfComment(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
blender_comment = blender_topic.comments.get(self.comment_guid)
topic = bcfxml.topics[blender_topic.name]
@@ -1104,7 +1092,7 @@ class EditBcfComment(bpy.types.Operator):
if comment.guid == self.comment_guid:
comment.comment = blender_comment.comment
comment.modified_date = XmlDateTime.now()
comment.modified_author = props.author
comment.modified_author = context.scene.BCFProperties.author
bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid)
return {"FINISHED"}
@@ -1117,7 +1105,7 @@ class AddBcfComment(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
if not props.comment:
cls.poll_message_set("No comment to add.")
return False
@@ -1140,7 +1128,7 @@ class AddBcfComment(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
comments = topic.comments
@@ -1148,7 +1136,7 @@ class AddBcfComment(bpy.types.Operator):
if bcf_v2:
comment = bcf.v2.model.Comment(
date=XmlDateTime.now(),
author=props.author,
author=context.scene.BCFProperties.author,
comment=props.comment,
guid=str(uuid.uuid4()),
)
@@ -1161,7 +1149,7 @@ class AddBcfComment(bpy.types.Operator):
else:
comment = bcf.v3.model.Comment(
date=XmlDateTime.now(),
author=props.author,
author=context.scene.BCFProperties.author,
comment=props.comment,
guid=str(uuid.uuid4()),
)
@@ -1191,7 +1179,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
if blender_topic is None:
cls.poll_message_set("No topic is active.")
@@ -1205,11 +1193,11 @@ class ActivateBcfViewpoint(bpy.types.Operator):
return True
def execute(self, context):
self.file = tool.Ifc.get()
self.file = IfcStore.get_file()
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
if self.viewpoint_guid:
@@ -1315,7 +1303,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
[0, 0, 0, 1],
)
)
props = tool.Georeference.get_georeference_props()
props = context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
matrix = ifcopenshell.util.geolocation.global2local(
@@ -1354,7 +1342,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
objs: list[bpy.types.Object] = []
for global_id in exception_global_ids:
obj = tool.Ifc.get_object_by_identifier(global_id)
obj = IfcStore.get_element(global_id)
if obj and context.view_layer.objects.get(obj.name):
assert isinstance(obj, bpy.types.Object)
objs.append(obj)
@@ -1390,14 +1378,16 @@ class ActivateBcfViewpoint(bpy.types.Operator):
def set_view_setup_hints(
self, viewpoint: bcf.agnostic.visinfo.VisualizationInfoHandler, context: bpy.types.Context
) -> None:
# TODO: handle view_setup_hints.openings_visible
# should we reload elements with/without opening applied here or ...?
if view_setup_hints := tool.Bcf.get_viewpoint_view_setup_hints(viewpoint):
pass
if not view_setup_hints.spaces_visible:
if viewpoint.visualization_info.components.view_setup_hints:
if not viewpoint.visualization_info.components.view_setup_hints.spaces_visible:
self.hide_spaces(context)
if viewpoint.visualization_info.components.view_setup_hints.openings_visible is not None:
self.set_openings_visibility(
viewpoint.visualization_info.components.view_setup_hints.openings_visible, context
)
else:
self.hide_spaces(context)
self.set_openings_visibility(False, context)
def hide_spaces(self, context: bpy.types.Context) -> None:
old = context.area.type
@@ -1407,13 +1397,16 @@ class ActivateBcfViewpoint(bpy.types.Operator):
bpy.ops.object.hide_view_set()
context.area.type = old
def set_openings_visibility(self, is_visible, context):
pass # We no longer have an openings collection
def set_selection(self, viewpoint: bcf.agnostic.visinfo.VisualizationInfoHandler) -> None:
selected_global_ids = viewpoint.get_selected_guids()
if selected_global_ids is None:
return
bpy.ops.object.select_all(action="DESELECT")
for global_id in selected_global_ids:
obj = tool.Ifc.get_object_by_identifier(global_id)
obj = IfcStore.get_element(global_id)
if obj:
obj.select_set(True)
obj.hide_set(False)
@@ -1426,7 +1419,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
for acomponent in acoloring.component:
global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color)
for global_id, color in global_id_colours.items():
obj = tool.Ifc.get_object_by_identifier(global_id)
obj = IfcStore.get_element(global_id)
if obj:
obj.color = self.hex_to_rgb(color)
@@ -1529,51 +1522,58 @@ class OpenBcfReferenceLink(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
props = tool.Bcf.get_bcf_props()
webbrowser.open(props.topic_links[self.index].name)
webbrowser.open(context.scene.BCFProperties.topic_links[self.index].name)
return {"FINISHED"}
class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper):
class SelectBcfHeaderFile(bpy.types.Operator):
bl_idname = "bim.select_bcf_header_file"
bl_label = "Select BCF Header File"
bl_description = "Select filepath for BCF header reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
if self.filepath:
props = tool.Bcf.get_bcf_props()
props.file_reference = self.filepath
context.scene.BCFProperties.file_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper):
class SelectBcfBimSnippetReference(bpy.types.Operator):
bl_idname = "bim.select_bcf_bim_snippet_reference"
bl_label = "Select BCF BIM Snippet Reference"
bl_description = "Select filepath for BCF snippet reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
if self.filepath:
props = tool.Bcf.get_bcf_props()
props.bim_snippet_reference = self.filepath
context.scene.BCFProperties.bim_snippet_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper):
class SelectBcfDocumentReference(bpy.types.Operator):
bl_idname = "bim.select_bcf_document_reference"
bl_label = "Select BCF Document Reference"
bl_description = "Select filepath for BCF document reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
if self.filepath:
props = tool.Bcf.get_bcf_props()
props.document_reference = self.filepath
context.scene.BCFProperties.document_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class LoadBcfHeaderIfcFile(bpy.types.Operator):
bl_idname = "bim.load_bcf_header_ifc_file"
@@ -1590,8 +1590,7 @@ class LoadBcfHeaderIfcFile(bpy.types.Operator):
assert bcfxml
bcf_path = tool.Bcf.get_path()
props = tool.Bcf.get_bcf_props()
topic = bcfxml.topics[props.active_topic.name]
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
entity = tool.Bcf.get_topic_header_files(topic)[self.index]
ifc_path = bcf.agnostic.topic.extract_file(topic, entity)
bpy.ops.bim.load_project(filepath=ifc_path)
@@ -1611,8 +1610,7 @@ class ExtractBcfFile(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = tool.Bcf.get_bcf_props()
topic = bcfxml.topics[props.active_topic.name]
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
if self.entity_type == "HEADER_FILE":
entity = tool.Bcf.get_topic_header_files(topic)[self.index]
+8 -79
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from . import bcfstore
from bonsai.bim.prop import StrProperty
from bpy.types import PropertyGroup
@@ -35,7 +34,6 @@ from functools import partial
from typing import Literal
from typing_extensions import assert_never
from bcf.agnostic.extensions import get_extensions_attributes
from typing import TYPE_CHECKING, Union
bcfviewpoints_enum = None
@@ -97,7 +95,7 @@ def getBcfViewpoints(self, context, force_update=False):
global bcfviewpoints_enum
if bcfviewpoints_enum is None or force_update: # Retrieving Viewpoints is slow. Make sure we only do when needed
bcfviewpoints_enum = []
props = tool.Bcf.get_bcf_props()
props = context.scene.BCFProperties
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
topic = props.active_topic
@@ -112,12 +110,6 @@ class BcfBimSnippet(PropertyGroup):
type: StringProperty(name="Type")
is_external: BoolProperty(name="Is External")
if TYPE_CHECKING:
schema: str
reference: str
type: str
is_external: bool
class BcfDocumentReference(PropertyGroup):
reference: StringProperty(name="Reference")
@@ -125,12 +117,6 @@ class BcfDocumentReference(PropertyGroup):
guid: StringProperty(name="GUID")
is_external: BoolProperty(name="Is External")
if TYPE_CHECKING:
reference: str
description: str
guid: str
is_external: bool
class BcfComment(PropertyGroup):
name: StringProperty(name="GUID")
@@ -142,16 +128,6 @@ class BcfComment(PropertyGroup):
modified_author: StringProperty(name="Modified Author")
is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfCommentIsEditable)
if TYPE_CHECKING:
name: str
date: str
author: str
comment: str
viewpoint: str
modified_date: str
modified_author: str
is_editable: bool
def get_extensions_items(
self: "BCFProperties", context: bpy.types.Context, edit_text: str, extensions_attr: str
@@ -207,44 +183,20 @@ class BcfTopic(PropertyGroup):
comments: CollectionProperty(name="Comments", type=BcfComment)
is_editable: BoolProperty(name="Edit Topic Attributes", default=False, update=updateBcfTopicIsEditable)
if TYPE_CHECKING:
name: str
title: str
type: str
status: str
priority: str
stage: str
creation_date: str
creation_author: str
modified_date: str
modified_author: str
assigned_to: str
due_date: str
description: str
viewpoints: str
files: bpy.types.bpy_prop_collection_idprop[StrProperty]
reference_links: bpy.types.bpy_prop_collection_idprop[BcfReferenceLink]
labels: bpy.types.bpy_prop_collection_idprop[BcfLabel]
bim_snippet: BcfBimSnippet
document_references: bpy.types.bpy_prop_collection_idprop[BcfDocumentReference]
related_topics: bpy.types.bpy_prop_collection_idprop[StrProperty]
comments: bpy.types.bpy_prop_collection_idprop[BcfComment]
is_editable: bool
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global RELATED_TOPICS_ENUM_ITEMS
props = self
active_topic = props.active_topic
active_related_topics = active_topic.related_topics.keys()
RELATED_TOPICS_ENUM_ITEMS = []
enum_items = []
i = 0
for t in props.topics:
if t.name == active_topic.name:
continue
if t.name in active_related_topics:
continue
RELATED_TOPICS_ENUM_ITEMS.append((t.name, t.title, t.description))
return RELATED_TOPICS_ENUM_ITEMS
enum_items.append((t.name, t.title, t.description))
return enum_items
class BCFProperties(PropertyGroup):
@@ -283,30 +235,7 @@ class BCFProperties(PropertyGroup):
comment: StringProperty(default="", name="Comment")
has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False)
if TYPE_CHECKING:
bcf_file: str
bcf_version: str
comment_text_width: int
name: str
author: str
topics: bpy.types.bpy_prop_collection_idprop[BcfTopic]
active_topic_index: int
file_reference: str
file_ifc_project: str
file_ifc_spatial_structure_element: str
reference_link: str
label: str
bim_snippet_reference: str
bim_snippet_type: str
bim_snippet_schema: str
document_reference: str
document_reference_description: str
document_description: str
related_topic: str
comment: str
has_related_viewpoint: bool
def clear_input_fields(self) -> None:
def clear_input_fields(self):
self.file_reference = ""
self.file_ifc_project = ""
self.file_ifc_spatial_structure_element = ""
@@ -321,7 +250,7 @@ class BCFProperties(PropertyGroup):
self.has_related_viewpoint = False
@property
def active_topic(self) -> Union[BcfTopic, None]:
def active_topic(self):
if len(self.topics) == 0:
return None
if self.active_topic_index < 0:
@@ -330,5 +259,5 @@ class BCFProperties(PropertyGroup):
self.active_topic_index = len(self.topics) - 1
return self.topics[self.active_topic_index]
def refresh_topic(self, context: bpy.types.Context) -> None:
def refresh_topic(self, context):
refreshBcfTopic(self, context)
+10 -6
View File
@@ -37,7 +37,8 @@ class BIM_PT_bcf(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
props = tool.Bcf.get_bcf_props()
scene = context.scene
props = scene.BCFProperties
if not bcfstore.BcfStore.get_bcfxml():
row = layout.row(align=True)
@@ -62,17 +63,18 @@ class BIM_PT_bcf(Panel):
row = layout.row()
row.prop(props, "author")
props = context.scene.BCFProperties
row = layout.row()
row.template_list("BIM_UL_topics", "", props, "topics", props, "active_topic_index")
col = row.column(align=True)
col.operator("bim.add_bcf_topic", icon="ADD", text="")
col.operator("bim.remove_bcf_topic", icon="REMOVE", text="")
topic = props.active_topic
if topic is not None:
if props.active_topic_index < len(props.topics):
topic = props.active_topic
is_editable = topic.is_editable
col.prop(topic, "is_editable", icon="CHECKMARK" if topic.is_editable else "GREASEPENCIL", icon_only=True)
topic = props.active_topic
row = layout.row()
row.enabled = is_editable
row.prop(topic, "description", text="")
@@ -129,7 +131,8 @@ class BIM_PT_bcf_metadata(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
props = tool.Bcf.get_bcf_props()
scene = context.scene
props = scene.BCFProperties
bcfxml = bcfstore.BcfStore.get_bcfxml()
if not bcfxml or props.active_topic_index >= len(props.topics):
@@ -293,7 +296,8 @@ class BIM_PT_bcf_comments(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
props = tool.Bcf.get_bcf_props()
scene = context.scene
props = scene.BCFProperties
if props.active_topic_index >= len(props.topics):
layout.label(text="No BCF project is loaded")
@@ -49,9 +49,9 @@ classes = (
def register():
bpy.types.Scene.BIMBoundaryProperties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
bpy.types.Object.BIMBoundaryProperties = bpy.props.PointerProperty(type=prop.BIMObjectBoundaryProperties)
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMObjectBoundaryProperties)
def unregister():
del bpy.types.Scene.BIMBoundaryProperties
del bpy.types.Object.BIMBoundaryProperties
del bpy.types.Object.bim_boundary_properties
@@ -43,8 +43,6 @@ class BoundaryDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -80,8 +78,7 @@ class BoundaryDecorator:
unselected_edges = []
unselected_tris = []
props = tool.Boundary.get_boundary_props()
for boundary in props.boundaries:
for boundary in context.scene.BIMBoundaryProperties.boundaries:
obj = boundary.obj
if not obj or not obj.data: # A boundary may not have data if it has no connection geometry
continue
+94 -140
View File
@@ -25,8 +25,6 @@ import mathutils
import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.api.boundary
import ifcopenshell.api.root
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
@@ -42,7 +40,16 @@ from bonsai.bim.module.model.decorator import ProfileDecorator
from bonsai.bim.module.boundary.decorator import BoundaryDecorator
import bonsai.core
import bonsai.core.geometry
from typing import Union, Optional
def get_boundaries_collection(blender_space):
space_collection = blender_space.BIMObjectProperties.collection
collection_name = f"Boundaries/{blender_space.BIMObjectProperties.ifc_definition_id}"
boundaries_collection = space_collection.children.get(collection_name)
if not boundaries_collection:
boundaries_collection = bpy.data.collections.new(collection_name)
space_collection.children.link(boundaries_collection)
return boundaries_collection
def disable_editing_boundary_geometry(context):
@@ -60,7 +67,7 @@ def disable_editing_boundary_geometry(context):
class Loader:
def __init__(self, operator: Optional[bpy.types.Operator] = None):
def __init__(self, operator=None):
self.operator = operator
self.ifc_file = None
self.logger = None
@@ -69,20 +76,17 @@ class Loader:
self.fallback_settings = self.load_fallback_settings()
self.load_importer()
def create_mesh(self, boundary: ifcopenshell.entity_instance) -> Union[bpy.types.Mesh, None]:
def create_mesh(self, boundary):
# ConnectionGeometry is optional in IFC schema for some reasons.
if not boundary.ConnectionGeometry:
return None
assert self.ifc_importer
assert self.operator
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
# Workaround for invalid geometry provided by Revit. See https://github.com/Autodesk/revit-ifc/issues/270
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
surface.InnerBoundaries = ()
try:
shape = ifcopenshell.geom.create_shape(self.settings, surface)
mesh = self.ifc_importer.create_mesh(boundary, shape)
assert mesh
mesh = self.ifc_importer.create_mesh(None, shape)
tool.Loader.link_mesh(shape, mesh)
except RuntimeError:
# Fallback solution for invalid geometry provided by Revit. (InnerBoundaries cuting OuterBoundary)
@@ -102,8 +106,7 @@ class Loader:
return None
mesh = bpy.data.meshes.new(str(surface.id()))
bm = bmesh.new()
coords = ifcopenshell.util.shape.get_vertices(shape)
verts = [bm.verts.new(coord) for coord in coords]
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
bm.faces.new(verts)
for inner_boundary in surface.InnerBoundaries:
try:
@@ -114,15 +117,13 @@ class Loader:
f"Skipping an inner boundary for IfcRelSpaceBoundary with ID {boundary.id()}. Geometry might be invalid",
)
return None
coords = ifcopenshell.util.shape.get_vertices(shape)
verts = [bm.verts.new(coord) for coord in coords]
bm.faces.new(verts)
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
for i in range(len(verts) - 1):
bm.edges.new(verts[i : i + 2])
bm.edges.new((verts[-1], verts[0]))
bm.to_mesh(mesh)
bm.free()
tool.Ifc.link(surface, mesh)
mesh.BIMMeshProperties.ifc_definition_id = surface.id()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
matrix = mathutils.Matrix(
ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist()
@@ -139,7 +140,7 @@ class Loader:
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
return settings
def load_importer(self) -> None:
def load_importer(self):
self.ifc_file = tool.Ifc.get()
self.logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger)
@@ -163,8 +164,9 @@ class Loader:
mesh = self.create_mesh(boundary)
obj = bpy.data.objects.new(f"{boundary.is_a()}/{boundary.Name}", mesh)
obj.matrix_world = blender_space.matrix_world
boundaries_collection = get_boundaries_collection(blender_space)
boundaries_collection.objects.link(obj)
tool.Ifc.link(boundary, obj)
tool.Collector.assign(obj)
return obj
@@ -288,7 +290,7 @@ class SelectProjectBoundaries(bpy.types.Operator):
return {"FINISHED"}
def get_colour(ifc_boundary: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
def get_colour(ifc_boundary):
"""Return a color depending on IfcClass given"""
product_colors = {
"IfcWall": (0.7, 0.3, 0, 1),
@@ -324,8 +326,7 @@ class ColourByRelatedBuildingElement(bpy.types.Operator):
def _execute(self, context):
for obj in context.visible_objects:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRelSpaceBoundary"):
@@ -364,9 +365,7 @@ class EnableEditingBoundary(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
bprops = context.active_object.bim_boundary_properties
bprops.is_editing = True
boundary = tool.Ifc.get_entity(context.active_object)
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
@@ -385,9 +384,7 @@ class DisableEditingBoundary(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
bprops = context.active_object.bim_boundary_properties
bprops.is_editing = False
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
setattr(bprops, blender_property, None)
@@ -400,10 +397,8 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
boundary = tool.Ifc.get_entity(obj)
bprops = context.active_object.bim_boundary_properties
boundary = tool.Ifc.get_entity(context.active_object)
attributes = dict()
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
obj = getattr(bprops, blender_property, None)
@@ -426,7 +421,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
tool.Boundary.move_origin_to_space_origin(context.active_object)
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings)
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
return {"FINISHED"}
@@ -535,7 +530,6 @@ class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Boundary.get_boundary_props()
to_delete = set()
spaces = set()
for obj in context.selected_objects:
@@ -555,7 +549,7 @@ class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
for boundary, boundary_obj in to_delete:
tool.Ifc.unlink(element=boundary)
bpy.data.objects.remove(boundary_obj)
props.boundaries.clear()
context.scene.BIMBoundaryProperties.boundaries.clear()
return {"FINISHED"}
@@ -566,7 +560,7 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Boundary.get_boundary_props()
props = context.scene.BIMBoundaryProperties
# filter not decorated boundaries and add decorations for them
decorated_boundaries = set([i.obj for i in props.boundaries])
active_boundaries = set()
@@ -592,70 +586,47 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boundary"
bl_label = "Add Boundary"
bl_description = (
"There are two options for this operator:\n"
"- just 1 IfcSpace is selected - generate a boundary based on "
"potential nearby (0.1m) boundary elements.\n"
"- 1 IfcSpace and 1 IfcElement are selected - generate a boundary "
"for IfcSpace and use selected IfcElement as boundary's building element."
)
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
parent_boundaries: list[ifcopenshell.entity_instance] = []
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
parent_boundaries = []
objs = tool.Blender.get_selected_objects()
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
if len(ifc_objects) not in (1, 2):
self.report({"ERROR"}, "Please select one or two IFC elements.")
return {"CANCELLED"}
objs = context.selected_objects
if not any(element.is_a("IfcSpace") for element in ifc_objects):
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
return {"CANCELLED"}
if not any((element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs):
self.report({"INFO"}, "No IfcSpace elements selected.")
return {"FINISHED"}
if len(objs) == 2:
# Validate input.
space = next((e for e in ifc_objects.keys() if e.is_a("IfcSpace")), None)
if not space:
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
return {"CANCELLED"}
element = next((e for e in ifc_objects.keys() if e.is_a("IfcElement")), None)
if not element:
self.report({"ERROR"}, "One of the selected objects must be an IfcElement.")
return {"CANCELLED"}
# The user may select two objects, a space and its related building element
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = obj
else:
related_building_element = element
related_building_element_obj = obj
parent_boundary = self.create_element_boundary(
context, space, ifc_objects[space], element, ifc_objects[element]
context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
)
if not parent_boundary:
self.report({"ERROR"}, "Failed to create a boundary.")
return {"FINISHED"}
parent_boundaries.append(parent_boundary)
if parent_boundary:
parent_boundaries.append(parent_boundary)
elif len(objs) == 1:
space = next(iter(ifc_objects))
if not space.is_a("IfcSpace"):
self.report({"ERROR"}, "1 element selected but it's not an IfcSpace - please, select IfcSpace..")
return {"CANCELLED"}
# New prototype, old code not yet removed. Still testing.
res = self.auto_generate_boundaries(space, ifc_objects[space])
if isinstance(res, str):
self.report({"ERROR"}, res)
return {"FINISHED"}
parent_boundaries.extend(res)
elif len(objs) == 1 and False:
space = tool.Ifc.get_entity(objs[0])
if space.is_a("IfcSpace"):
self.auto_generate_boundaries(space, objs[0])
elif len(objs) == 1:
# Optionally the user may select just the space, and the building element shall be auto-detected
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
def msg(self, context):
self.layout.label(text="Please set an active container to detect space boundaries from.")
@@ -692,17 +663,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
obj = tool.Ifc.get_object(parent_boundary)
obj.select_set(True)
def auto_generate_boundaries(
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
) -> Union[str, list[ifcopenshell.entity_instance]]:
"""
:return: list of created boundaries or a string with error description.
"""
ifc_file = tool.Ifc.get()
props = tool.Model.get_model_props()
boundaries: list[ifcopenshell.entity_instance] = []
assert isinstance(space_obj.data, bpy.types.Mesh)
def auto_generate_boundaries(self, space, space_obj):
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = (
@@ -722,7 +683,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
shapes = {}
settings = ifcopenshell.geom.settings()
settings.set("disable-opening-subtractions", True)
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
if iterator.initialize():
while True:
tree.add_element(iterator.get_native())
@@ -735,12 +696,12 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
if not building_elements:
return "No building elements found to create boundaries."
return
# Create a dissolved bmesh for the space
space_bm = bmesh.new()
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts, edges=space_bm.edges)
# Create dissolved bmeshes for all boundary elements
building_element_bms = {}
@@ -818,7 +779,9 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# we cheat by using the exterior boundary to mean "gross".
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
parent_boundary = tool.Ifc.run(
"root.create_entity", ifc_class=bpy.context.scene.BIMModelProperties.boundary_class
)
if building_element.is_a("IfcVirtualElement"):
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
@@ -850,7 +813,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
exterior_boundary_polygon, space_face_matrix
)
self.set_boundary_name(parent_boundary)
boundaries.append(parent_boundary)
for rel in getattr(building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
@@ -859,6 +821,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# Create shape of opening as a dissolved BMesh
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, opening)
m = shape.transformation.matrix
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
mat.translation = (0, 0, 0)
opening_bm = bmesh.new()
@@ -896,7 +859,9 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
continue
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
boundary = tool.Ifc.run(
"root.create_entity", ifc_class=bpy.context.scene.BIMModelProperties.boundary_class
)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling or opening
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
@@ -910,39 +875,28 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if boundary.is_a() != "IfcRelSpaceBoundary":
boundary.ParentBoundary = parent_boundary
self.set_boundary_name(boundary)
boundaries.append(boundary)
return boundaries
def create_element_boundary(
self,
context: bpy.types.Context,
space: ifcopenshell.entity_instance,
space_obj: bpy.types.Object,
building_element: ifcopenshell.entity_instance,
building_element_obj: bpy.types.Object,
) -> Union[ifcopenshell.entity_instance, None]:
"""Create element boundary for IfcSpace and IfcElement as building element.
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
):
if not relating_space or not related_building_element:
return
:return: IfcRelSpaceBoundary or ``None`` if couldn't find a face on ``space_obj``
that would be close enough to ``related_building_element_obj``.
"""
props = tool.Model.get_model_props()
# Find which face on space should be bounded to related building element
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
space_bm = bmesh.new()
assert isinstance(space_obj.data, bpy.types.Mesh)
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
bm = bmesh.new()
bm.from_mesh(relating_space_obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
target_distance = inf
target_face = None
to_building_obj_space = building_element_obj.matrix_world.inverted()
for face in space_bm.faces:
centroid = space_obj.matrix_world @ face.calc_center_median()
raycast = building_element_obj.closest_point_on_mesh(to_building_obj_space @ centroid, distance=1)
for face in bm.faces:
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
raycast = related_building_element_obj.closest_point_on_mesh(
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
)
if raycast[0]:
distance = (building_element_obj.matrix_world @ raycast[1] - centroid).length
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
if distance < target_distance:
target_face = face
target_distance = distance
@@ -957,7 +911,9 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
related_building_element_polygon = self.get_flattened_polygon(building_element, space_obj, target_face_matrix_i)
related_building_element_polygon = self.get_flattened_polygon(
related_building_element, relating_space_obj, target_face_matrix_i
)
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
@@ -973,8 +929,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
):
return
ifc_file = tool.Ifc.get()
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
# internal space
@@ -987,14 +942,16 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# space.
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
for rel in getattr(building_element, "HasOpenings", []):
inner_boundaries = []
for rel in getattr(related_building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
if not opening.HasFillings:
continue
# TODO: HasFilling is a zero to many relationship. How to handle many ?
filling = opening.HasFillings[0].RelatedBuildingElement
opening_polygon = self.get_flattened_polygon(opening, space_obj, target_face_matrix_i)
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
# An inner boundary is supposed to overlap its parent boundary according to IFC4 documentation
# so we extend our exterior boundary with all opening which has a filling
@@ -1009,11 +966,11 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
continue
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
boundary.RelatingSpace = relating_space
boundary.RelatedBuildingElement = filling
boundary.ConnectionGeometry = connection_geometry
if building_element.is_a("IfcVirtualElement"):
if related_building_element.is_a("IfcVirtualElement"):
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
@@ -1025,23 +982,20 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
connection_geometry = self.create_connection_geometry_from_polygon(
exterior_boundary_polygon, target_face_matrix
)
parent_boundary.RelatingSpace = space
parent_boundary.RelatedBuildingElement = building_element
parent_boundary.RelatingSpace = relating_space
parent_boundary.RelatedBuildingElement = related_building_element
parent_boundary.ConnectionGeometry = connection_geometry
self.set_boundary_name(parent_boundary)
return parent_boundary
def get_face_matrix(self, p1: Vector, p2: Vector, p3: Vector) -> Matrix:
def get_face_matrix(self, p1, p2, p3):
edge1 = p2 - p1
edge2 = p3 - p1
normal = edge1.cross(edge2)
assert isinstance(normal, Vector)
z_axis = normal.normalized()
x_axis = p2 - p1
x_axis.normalize()
y_axis = z_axis.cross(x_axis)
assert isinstance(y_axis, Vector)
mat = Matrix()
mat.col[0][:3] = x_axis
@@ -1111,7 +1065,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
return surface
def set_boundary_name(self, boundary: ifcopenshell.entity_instance):
def set_boundary_name(self, boundary):
"""
By convention 1stLevel and 2ndLevel boundary have specific name and description
See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRelSpaceBoundary.htm
+4 -15
View File
@@ -30,24 +30,23 @@ from bpy.props import (
CollectionProperty,
)
import bonsai.tool as tool
from typing import TYPE_CHECKING, Union
def space_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object) -> bool:
def space_filter(self, object):
entity = tool.Ifc.get_entity(object)
if entity:
return entity.is_a("IfcSpace") or entity.is_a("IfcExternalSpatialElement")
return False
def boundary_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object) -> bool:
def boundary_filter(self, object):
entity = tool.Ifc.get_entity(object)
if entity:
return entity.is_a("IfcRelSpaceBoundary")
return False
def element_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object) -> bool:
def element_filter(self, object):
entity = tool.Ifc.get_entity(object)
if entity:
return entity.is_a("IfcElement")
@@ -61,16 +60,6 @@ class BIMObjectBoundaryProperties(PropertyGroup):
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
if TYPE_CHECKING:
is_editing: bool
relating_space: Union[bpy.types.Object, None]
related_building_element: Union[bpy.types.Object, None]
parent_boundary: Union[bpy.types.Object, None]
corresponding_boundary: Union[bpy.types.Object, None]
class BIMBoundaryProperties(PropertyGroup):
boundaries: bpy.props.CollectionProperty(type=ObjProperty, description="Decorated boundaries")
if TYPE_CHECKING:
boundaries: bpy.types.bpy_prop_collection_idprop[ObjProperty]
boundaries: bpy.props.CollectionProperty(type=ObjProperty)
+12 -15
View File
@@ -18,6 +18,7 @@
import bpy
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
import bonsai.tool as tool
from bonsai.bim.module.boundary.data import SpaceBoundariesData
@@ -33,7 +34,7 @@ class BIM_PT_SceneBoundaries(Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return IfcStore.get_file()
def draw(self, context):
row = self.layout.row(align=True)
@@ -52,23 +53,21 @@ class BIM_PT_Boundary(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
if not IfcStore.get_element(props.ifc_definition_id):
return False
entity = tool.Ifc.get().by_id(props.ifc_definition_id)
entity = IfcStore.get_file().by_id(props.ifc_definition_id)
return entity.is_a("IfcRelSpaceBoundary")
def draw(self, context):
obj = context.active_object
assert obj
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
ifc_file = tool.Ifc.get()
boundary = ifc_file.by_id(props.ifc_definition_id)
self.bprops = tool.Boundary.get_object_boundary_props(obj)
self.bprops = context.active_object.bim_boundary_properties
if self.bprops.is_editing:
row = self.layout.row(align=True)
row.operator("bim.edit_boundary_attributes", icon="CHECKMARK", text="Save Attributes")
@@ -128,12 +127,12 @@ class BIM_PT_SpaceBoundaries(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
if not IfcStore.get_element(props.ifc_definition_id):
return False
element = tool.Ifc.get_entity(context.active_object)
for ifc_class in ("IfcSpace", "IfcExternalSpatialElement"):
@@ -145,9 +144,7 @@ class BIM_PT_SpaceBoundaries(Panel):
if not SpaceBoundariesData.is_loaded:
SpaceBoundariesData.load()
obj = context.active_object
assert obj
self.props = tool.Blender.get_object_bim_props(obj)
self.props = context.active_object.BIMObjectProperties
self.ifc_file = tool.Ifc.get()
row = self.layout.row()
row.operator("bim.load_space_boundaries")
+1 -1
View File
@@ -52,7 +52,7 @@ class BrickschemaData:
def active_relations(cls):
if BrickStore.graph is None:
return []
props = tool.Brick.get_brick_props()
props = bpy.context.scene.BIMBrickProperties
try:
brick = props.bricks[props.active_brick_index]
except:
+21 -21
View File
@@ -22,26 +22,29 @@ import ifcopenshell.api
import bonsai.tool as tool
import bonsai.core.brick as core
import bonsai.bim.handler
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
from bonsai.tool.brick import BrickStore
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_brick_project"
bl_label = "Load Brickschema Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load in a Brick project from a file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filename_ext = ".ttl"
def _execute(self, context):
if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower():
root = tool.Brick.get_brick_props().brick_list_root
root = context.scene.BIMBrickProperties.brick_list_root
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
else:
self.report({"ERROR"}, f"Failed to load {self.filepath}")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.view_brick_class"
@@ -108,10 +111,10 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Assign the selected Ifc entity to the selected Brick entity"
def _execute(self, context):
if not (obj := context.active_object) or not (element := tool.Ifc.get_entity(obj)):
if not context.active_object:
self.report({"ERROR"}, f"No Ifc selected")
return
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
try:
props.bricks[props.active_brick_index]
except:
@@ -120,7 +123,7 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
core.assign_brick_reference(
tool.Ifc,
tool.Brick,
element=element,
element=tool.Ifc.get_entity(context.active_object),
library=tool.Ifc.get().by_id(int(props.libraries)),
brick_uri=props.bricks[props.active_brick_index].uri,
)
@@ -133,7 +136,7 @@ class AddBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick entity"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
core.add_brick(
tool.Ifc,
tool.Brick,
@@ -152,7 +155,7 @@ class AddBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick relationship"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
brick = props.bricks[props.active_brick_index]
if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
object = props.new_brick_relation_object
@@ -170,7 +173,7 @@ class ConvertIfcToBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Convert Ifc entities and relations to Brick entities and relations"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
library = None
if props.libraries:
library = tool.Ifc.get().by_id(int(props.libraries))
@@ -198,8 +201,7 @@ class NewBrickFile(bpy.types.Operator):
return {"FINISHED"}
def _execute(self, context):
props = tool.Brick.get_brick_props()
root = props.brick_list_root
root = context.scene.BIMBrickProperties.brick_list_root
core.new_brick_file(tool.Brick, brick_root=root)
def rollback(self, data):
@@ -228,7 +230,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Delete this entity"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
core.remove_brick(
tool.Ifc,
tool.Brick,
@@ -237,20 +239,18 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
)
class SerializeBrick(bpy.types.Operator, ExportHelper):
class SerializeBrick(bpy.types.Operator):
bl_idname = "bim.serialize_brick"
bl_label = "Serialize Brick"
# Prevents crash on Blender 4.4.0.
bl_description = "Save active Brick project by the provided filepath."
filename_ext = ".ttl"
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
def invoke(self, context, event):
if self.should_save_as or not BrickStore.path:
return ExportHelper.invoke(self, context, event)
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
else:
return self.execute(context)
@@ -273,7 +273,7 @@ class AddBrickNamespace(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Bind a new namespace to the Brick project"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
alias = props.new_brick_namespace_alias
uri = props.new_brick_namespace_uri
core.add_brick_namespace(tool.Brick, alias=alias, uri=uri)
@@ -288,6 +288,6 @@ class RemoveBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
object: bpy.props.StringProperty(name="Object")
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
brick = props.bricks[props.active_brick_index]
core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object)
+11 -49
View File
@@ -33,7 +33,6 @@ from bpy.props import (
import bonsai.core.brick as core
import bonsai.tool.brick as tool
from bonsai.tool.brick import BrickStore
from typing import TYPE_CHECKING
def update_active_brick_index(self, context):
@@ -47,41 +46,34 @@ def get_libraries(self, context):
def get_namespaces(self, context):
global NAMESPACES_ENUM_ITEMS
NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
return NAMESPACES_ENUM_ITEMS
return [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
def get_brick_entity_classes(self, context):
global ENTITY_CLASSES_ENUM_ITEMS
entity = self.brick_entity_create_type
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
return ENTITY_CLASSES_ENUM_ITEMS
return [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
def get_brick_roots(self, context):
global BRICK_ROOTS_ENUM_ITEMS
BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes]
return BRICK_ROOTS_ENUM_ITEMS
return [(root, root, "") for root in BrickStore.root_classes]
def get_brick_relations(self, context):
global BRICK_RELATIONS_ENUM_ITEMS
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
relations = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
for relation in BrickschemaData.data["active_relations"]:
if relation["predicate_name"] == "label":
return BRICK_RELATIONS_ENUM_ITEMS
BRICK_RELATIONS_ENUM_ITEMS.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", ""))
return BRICK_RELATIONS_ENUM_ITEMS
return relations
relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", ""))
return relations
def update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.brick_list_root
def update_view(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False)
def split_screen_update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.split_screen_brick_list_root
def split_screen_update_view(self, context):
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True)
@@ -91,11 +83,6 @@ class Brick(PropertyGroup):
uri: StringProperty(name="URI")
total_items: IntProperty(name="Total Items")
if TYPE_CHECKING:
label: str
uri: str
total_items: int
class BIMBrickProperties(PropertyGroup):
active_brick_class: StringProperty(name="Active Brick Class")
@@ -130,28 +117,3 @@ class BIMBrickProperties(PropertyGroup):
split_screen_brick_list_root: EnumProperty(
name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view
)
if TYPE_CHECKING:
active_brick_class: str
brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
bricks: bpy.types.bpy_prop_collection_idprop[Brick]
active_brick_index: int
libraries: str
set_list_root_toggled: bool
brick_list_root: str
namespace: str
new_brick_namespace_alias: str
new_brick_namespace_uri: str
new_brick_label: str
brick_entity_create_type: str
brick_entity_class: str
brick_create_relations_toggled: bool
brick_edit_relations_toggled: bool
new_brick_relation_type: str
new_brick_relation_object: str
split_screen_toggled: bool
split_screen_bricks: bpy.types.bpy_prop_collection_idprop[Brick]
split_screen_active_brick_index: int
split_screen_active_brick_class: str
split_screen_brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
split_screen_brick_list_root: str
+7 -19
View File
@@ -45,7 +45,7 @@ class BIM_PT_brickschema_project_info(Panel):
bl_parent_id = "BIM_PT_brickschema"
def draw(self, context):
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
if not BrickschemaData.data["is_loaded"]:
row = self.layout.row(align=True)
@@ -87,7 +87,7 @@ class BIM_PT_brickschema_namespaces(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
row = self.layout.row(align=True)
row.label(text="Active Namespace:")
@@ -122,7 +122,7 @@ class BIM_PT_brickschema_create_entity(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
# TO DO: hide this if selected entity already has a reference, or something similar
row = self.layout.row(align=True)
@@ -156,7 +156,7 @@ class BIM_PT_brickschema_viewport(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
row = self.layout.row(align=True)
row.column().alignment = "RIGHT"
@@ -178,14 +178,7 @@ class BIM_PT_brickschema_viewport(Panel):
row = grid_left.row()
BIM_UL_bricks.split_screen = False
row.template_list(
BIM_UL_bricks.__name__,
BIM_UL_bricks.__name__ + "_bricks",
self.props,
"bricks",
self.props,
"active_brick_index",
)
row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
if self.props.split_screen_toggled:
grid_right = grid.column(align=True)
@@ -202,12 +195,7 @@ class BIM_PT_brickschema_viewport(Panel):
row = grid_right.row()
BIM_UL_bricks.split_screen = True
row.template_list(
BIM_UL_bricks.__name__,
BIM_UL_bricks.__name__ + "_split_screen_bricks",
self.props,
"split_screen_bricks",
self.props,
"split_screen_active_brick_index",
"BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index"
)
if BrickschemaData.data["active_relations"]:
@@ -304,7 +292,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
def draw(self, context):
if not BrickschemaReferencesData.is_loaded:
BrickschemaReferencesData.load()
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
if not BrickschemaReferencesData.data["is_loaded"]:
row = self.layout.row()
+1
View File
@@ -18,6 +18,7 @@
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
class BIM_PT_bsdd(Panel):
+50 -58
View File
@@ -25,7 +25,6 @@ import bonsai.tool as tool
from mathutils import Vector, Matrix
from math import pi, radians, sin, cos, sqrt
import ifcopenshell.util.unit
from typing import Union
messages = {
@@ -128,10 +127,9 @@ class CadMitre(bpy.types.Operator):
class CadFillet(bpy.types.Operator):
bl_idname = "bim.cad_fillet"
bl_label = "CAD Fillet"
bl_description = "Add fillet to the 2 selected edges."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
@classmethod
def poll(cls, context):
@@ -144,15 +142,18 @@ class CadFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
obj = bpy.context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
mesh = obj.data
bm = bmesh.from_edit_mesh(mesh)
selected_edges = [e for e in bm.edges if e.select]
if len(selected_edges) != 2:
self.report({"ERROR"}, "Exactly 2 edges should be selected.")
return {"CANCELLED"}
# Assume the user has selected two edges sharing a vert, but merge verts
@@ -217,7 +218,6 @@ class CadFillet(bpy.types.Operator):
class CadArcFrom2Points(bpy.types.Operator):
bl_idname = "bim.cad_arc_from_2_points"
bl_label = "CAD Arc from 2 Points"
bl_description = "Add an arc to the active mesh based on 2 selected vertices."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
@@ -234,7 +234,6 @@ class CadArcFrom2Points(bpy.types.Operator):
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
obj = bpy.context.active_object
assert obj
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
cursor = bpy.context.scene.cursor.location
@@ -242,12 +241,10 @@ class CadArcFrom2Points(bpy.types.Operator):
if not center:
return {"CANCELLED"}
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 2:
self.report({"ERROR"}, "Exactly 2 vertices should be selected.")
return {"CANCELLED"}
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
@@ -281,7 +278,6 @@ class CadArcFrom3Points(bpy.types.Operator):
bl_idname = "bim.cad_arc_from_3_points"
bl_label = "CAD Arc from 3 Points"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create a points based arc from 3 selected points."
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
only_recalculate_center: bpy.props.BoolProperty(name="Only Recalculate Center", default=False)
@@ -344,12 +340,11 @@ class CadArcFrom3Points(bpy.types.Operator):
class CadOffset(bpy.types.Operator):
bl_idname = "bim.cad_offset"
bl_label = "CAD Offset"
bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle."
bl_options = {"REGISTER", "UNDO"}
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
@classmethod
def poll(cls, context):
def poll(self, context):
return context.mode == "EDIT_MESH"
def draw(self, context):
@@ -372,6 +367,9 @@ class CadOffset(bpy.types.Operator):
# effect of converting an unclosed loop into a closed loop which is also
# not what users expect.
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.distance = self.distance * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
@@ -543,8 +541,7 @@ class CadOffset(bpy.types.Operator):
class AddIfcCircle(bpy.types.Operator):
bl_idname = "bim.add_ifccircle"
bl_label = "Add IfcCircle"
bl_description = "Add IfcCircle to the currently active mesh."
radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE")
radius: bpy.props.FloatProperty(name="Radius", default=0.5)
@classmethod
def poll(cls, context):
@@ -552,19 +549,18 @@ class AddIfcCircle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
if self.has_selected_existing_circle(context):
self.change_radius(context)
else:
self.create_circle(context)
return {"FINISHED"}
def has_selected_existing_circle(self, context: bpy.types.Context) -> bool:
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
def has_selected_existing_circle(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 2:
return False
@@ -577,32 +573,32 @@ class AddIfcCircle(bpy.types.Operator):
for group in groups:
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
return True
return False
def change_radius(self, context: bpy.types.Object) -> None:
bm = bmesh.from_edit_mesh(self.mesh)
def change_radius(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
center = verts[0].co.lerp(verts[1].co, 0.5)
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(self.mesh)
bmesh.update_edit_mesh(obj.data)
def create_circle(self, context: bpy.types.Context) -> None:
obj = self.obj
def create_circle(self, context):
obj = context.active_object
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
if len(obj.vertex_groups):
last_group = obj.vertex_groups[-1]
verts_in_group = [v for v in self.mesh.vertices if last_group.index in [vg.group for vg in v.groups]]
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
obj.vertex_groups.remove(last_group)
group = obj.vertex_groups.new(name="IFCCIRCLE")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(self.mesh)
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -619,15 +615,14 @@ class AddIfcCircle(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(self.mesh)
bmesh.update_edit_mesh(obj.data)
class AddIfcArcIndexFillet(bpy.types.Operator):
bl_idname = "bim.add_ifcarcindex_fillet"
bl_label = "Add Arc Index Fillet"
bl_description = "Add a fillet for the selected vertices."
bl_options = {"REGISTER", "UNDO"}
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
@classmethod
def poll(cls, context):
@@ -640,19 +635,18 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
if self.has_selected_existing_arc(context):
self.change_radius(context)
else:
return self.create_arc(context)
self.create_arc(context)
return {"FINISHED"}
def has_selected_existing_arc(self, context: bpy.types.Context) -> bool:
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
def has_selected_existing_arc(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 3:
return False
@@ -668,17 +662,15 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
return True
except:
pass # Potentially fail if the vert has been removed in the previous operation
return False
def change_radius(self, context: bpy.types.Context) -> None:
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
def change_radius(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
edges = [e for e in bm.edges if e.select and not e.hide]
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
v1 = edges[0].other_vert(mid)
v2 = edges[1].other_vert(mid)
assert v1 and v2
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
@@ -686,7 +678,6 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
assert v3 and v4
dir1 = (v3.co - v1.co).normalized()
dir2 = (v4.co - v2.co).normalized()
@@ -707,10 +698,11 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(self.mesh)
bmesh.update_edit_mesh(obj.data)
def create_arc(self, context):
obj = bpy.context.active_object
def create_arc(self, context: bpy.types.Context) -> set[str]:
obj = self.obj
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
@@ -722,7 +714,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
group = obj.vertex_groups.new(name="IFCARCINDEX")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(self.mesh)
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -741,7 +733,6 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
assert v1 and v2
dir1 = (v1.co - shared_vert.co).normalized()
dir2 = (v2.co - shared_vert.co).normalized()
edge_angle = dir1.angle(dir2)
@@ -775,8 +766,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(self.mesh)
return {"FINISHED"}
bmesh.update_edit_mesh(obj.data)
class AlignViewToProfile(bpy.types.Operator):
@@ -812,9 +802,8 @@ class AlignViewToProfile(bpy.types.Operator):
class AddRectangle(bpy.types.Operator):
bl_idname = "bim.add_rectangle"
bl_label = "Add Rectangle"
bl_description = "Add rectangle shape to the active mesh."
x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE")
x: bpy.props.FloatProperty(name="X", default=1)
y: bpy.props.FloatProperty(name="Y", default=1)
@classmethod
def poll(cls, context):
@@ -822,8 +811,11 @@ class AddRectangle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.x = self.x * si_conversion
self.y = self.y * si_conversion
obj = context.active_object
assert obj and isinstance(obj.data, bpy.types.Mesh)
bm = bmesh.from_edit_mesh(obj.data)
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
new_verts = [
+1 -9
View File
@@ -20,7 +20,6 @@ import bpy
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import PropertyGroup
from math import pi
from typing import TYPE_CHECKING
class BIMCadProperties(PropertyGroup):
@@ -32,11 +31,4 @@ class BIMCadProperties(PropertyGroup):
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
if TYPE_CHECKING:
resolution: int
radius: float
distance: float
x: float
y: float
gable_roof_edge_angle: float
gable_roof_separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
+101 -76
View File
@@ -23,13 +23,26 @@ import bonsai.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
from functools import partial
# TODO duplicate code in cad/workspace and model/workspace
def check_display_mode():
global display_mode
try:
theme = bpy.context.preferences.themes["Default"]
text_color = theme.user_interface.wcol_menu_item.text
if sum(text_color) < 2.6:
display_mode = "lm"
else:
display_mode = "dm"
except:
display_mode = "dm"
def load_custom_icons():
global custom_icon_previews, display_mode
global custom_icon_previews
if display_mode is None:
display_mode = tool.Blender.detect_icon_color_mode("user_interface.wcol_tool.text")
check_display_mode()
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
custom_icon_previews = bpy.utils.previews.new()
@@ -69,15 +82,12 @@ class CadTool(WorkSpaceTool):
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
)
def draw_settings(
context: bpy.types.Context, layout: bpy.types.UILayout, workspace_tool: bpy.types.WorkSpaceTool
) -> None:
def draw_settings(context, layout, workspace_tool):
ui_context = str(context.region.type)
obj = context.active_object
if not obj or not (data := obj.data):
if not obj or not obj.data:
return
is_profile = tool.Geometry.is_profile_object_active()
if is_profile:
if hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
element = tool.Ifc.get_entity(obj)
if element:
if element.is_a("IfcProfileDef"):
@@ -112,25 +122,20 @@ class CadTool(WorkSpaceTool):
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
add_layout_hotkey_operator(row, "Fillet", "S_V", "Fillet", 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__, ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
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, "Rectangle", "S_R", "Rectangle", ui_context)
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, "Circle", "S_C", "Circle", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", "3-Point Arc", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context)
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", "Reset Vertex", ui_context)
elif (
isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
and tool.Geometry.get_mesh_props(data).subshape_type == "AXIS"
):
add_header_apply_button(
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
)
elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS":
add_header_apply_button(layout, "Edit Axis", "bim.set_arc_index", "bim.set_arc_index", ui_context)
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)
@@ -138,15 +143,15 @@ class CadTool(WorkSpaceTool):
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
add_layout_hotkey_operator(row, "Fillet", "S_F", "Fillet", 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__, ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
else:
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["pset_data"]
and tool.Model.get_railing_props(obj).is_editing_path
and context.active_object.BIMRailingProperties.is_editing_path
):
add_header_apply_button(
layout,
@@ -159,7 +164,7 @@ class CadTool(WorkSpaceTool):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and tool.Model.get_roof_props(obj).is_editing_path
and context.active_object.BIMRoofProperties.is_editing_path
):
add_header_apply_button(
layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context
@@ -174,19 +179,19 @@ class CadTool(WorkSpaceTool):
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
add_layout_hotkey_operator(row, "Fillet", "S_V", "Fillet", 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__, ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
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, "2-Point Arc", "S_C", "2-Point Arc", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", "3-Point Arc", ui_context)
class CadHotkey(bpy.types.Operator):
bl_idname = "bim.cad_hotkey"
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "CAD Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -195,21 +200,19 @@ class CadHotkey(bpy.types.Operator):
return operator.description or ""
def execute(self, context):
self.props = tool.Cad.get_cad_props()
self.props = context.scene.BIMCadProperties
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def draw(self, context):
props = tool.Cad.get_cad_props()
obj = context.active_object
props = context.scene.BIMCadProperties
if self.hotkey == "S_C":
if tool.Geometry.is_profile_object_active():
if self.is_profile():
row = self.layout.row()
row.prop(props, "radius")
elif self.hotkey == "S_F":
if not tool.Geometry.is_profile_object_active():
if not self.is_profile():
row = self.layout.row()
row.prop(props, "resolution")
row = self.layout.row()
@@ -220,7 +223,7 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "distance")
elif self.hotkey == "S_R":
if tool.Geometry.is_profile_object_active():
if self.is_profile():
row = self.layout.row()
row.prop(props, "x")
row = self.layout.row()
@@ -228,18 +231,20 @@ class CadHotkey(bpy.types.Operator):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and tool.Model.get_roof_props(obj).is_editing_path
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
self.layout.row().prop(props, "gable_roof_separate_verts")
elif self.hotkey == "S_V":
if not tool.Geometry.is_profile_object_active():
if not self.is_profile():
row = self.layout.row()
row.prop(props, "resolution")
def hotkey_S_C(self):
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -247,69 +252,65 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
def hotkey_S_O(self):
bpy.ops.bim.cad_offset(distance=self.props.distance)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
def hotkey_S_Q(self):
obj = bpy.context.active_object
if not obj:
return
if not tool.Geometry.has_mesh_properties(data := obj.data):
return
element = tool.Ifc.get_entity(obj)
if not element:
return
mprops = tool.Geometry.get_mesh_props(data)
if mprops.subshape_type == "PROFILE":
element = tool.Ifc.get_entity(bpy.context.active_object)
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
if element.is_a("IfcProfileDef"):
bpy.ops.bim.edit_arbitrary_profile()
elif element.is_a("IfcRelSpaceBoundary"):
bpy.ops.bim.edit_boundary_geometry()
else:
bpy.ops.bim.edit_extrusion_profile()
elif mprops.subshape_type == "AXIS":
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
def hotkey_S_R(self):
obj = bpy.context.active_object
if not obj:
return
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
if self.is_profile():
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and tool.Model.get_roof_props(obj).is_editing_path
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
bpy.ops.bim.set_gable_roof_edge_angle(
angle=self.props.gable_roof_edge_angle, separate_verts=self.props.gable_roof_separate_verts
)
def hotkey_S_T(self):
bpy.ops.bim.cad_mitre()
def hotkey_S_V(self):
if tool.Geometry.is_profile_object_active():
if self.is_profile():
bpy.ops.bim.set_arc_index()
else:
bpy.ops.bim.cad_arc_from_3_points(resolution=self.props.resolution)
def hotkey_S_X(self):
if tool.Geometry.is_profile_object_active():
if self.is_profile():
bpy.ops.bim.reset_vertex()
def is_profile(self):
obj = bpy.context.active_object
return (
obj
and obj.data
and hasattr(obj.data, "BIMMeshProperties")
and obj.data.BIMMeshProperties.subshape_type == "PROFILE"
)
def add_header_apply_button(
layout: bpy.types.UILayout, text: str, apply_operator: str, cancel_operator: str, ui_context: str = ""
) -> None:
def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_context=""):
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
row = layout.row(align=True)
row.label(text=f"{text} Mode", icon_value=custom_icon)
@@ -328,8 +329,32 @@ def add_header_apply_button(
row.label(text="Tools")
add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="cad", module_name=__name__)
def add_layout_hotkey_operator(layout, text, hotkey, description, ui_context=""):
parts = hotkey.split("_")
modifier, key = parts
op_text = "" if ui_context == "TOOL_HEADER" else text
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True)
op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon)
if ui_context != "TOOL_HEADER" and len(parts) == 2:
layout = layout.row(align=True) # Create a new line for hotkey display
layout.label(text="", icon=modifier_icon)
layout.label(text="", icon=f"EVENT_{key}")
hotkey_description = f"Hotkey: {modifier_str} {key}"
description = "\n\n".join(filter(None, [hotkey_description if description else ""]))
op.hotkey = hotkey
op.description = description or hotkey_description
return op
custom_icon_previews = None
display_mode = None
MODIFIERS = {
"S": ("EVENT_SHIFT", ""),
}
@@ -49,8 +49,6 @@ class ClashDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -62,9 +60,8 @@ class ClashDecorator:
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
props = tool.Clash.get_clash_props()
text = props.active_clash_text
p = props.p1.lerp(props.p2, 0.5)
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
@@ -95,8 +92,7 @@ class ClashDecorator:
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
props = tool.Clash.get_clash_props()
selected_vertices = [props.p1, props.p2]
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
+84 -82
View File
@@ -24,20 +24,26 @@ import logging
import numpy as np
import ifcopenshell
import bonsai.tool as tool
from bpy_extras.io_utils import ExportHelper, ImportHelper
from math import radians
from mathutils import Matrix, Vector
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator
class ExportClashSets(bpy.types.Operator, ExportHelper):
class ExportClashSets(bpy.types.Operator):
bl_idname = "bim.export_clash_sets"
bl_label = "Export Clash Sets"
bl_description = "Export clash sets to a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = tool.Clash.export_clash_sets()
@@ -46,24 +52,26 @@ class ExportClashSets(bpy.types.Operator, ExportHelper):
return {"FINISHED"}
class ImportClashSets(bpy.types.Operator, ImportHelper):
class ImportClashSets(bpy.types.Operator):
bl_idname = "bim.import_clash_sets"
bl_label = "Import Clash Sets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import clash sets from a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
return ImportHelper.invoke(self, context, event)
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
tool.Clash.load_clash_sets(self.filepath)
props = tool.Clash.get_clash_props()
props.clash_sets.clear()
context.scene.BIMClashProperties.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
new = props.clash_sets.add()
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = clash_set["name"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
@@ -98,8 +106,7 @@ class AddClashSet(bpy.types.Operator):
bl_description = "Add a clash set"
def execute(self, context):
props = tool.Clash.get_clash_props()
new = props.clash_sets.add()
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = "New Clash Set"
return {"FINISHED"}
@@ -112,8 +119,7 @@ class RemoveClashSet(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
props = tool.Clash.get_clash_props()
props.clash_sets.remove(self.index)
context.scene.BIMClashProperties.clash_sets.remove(self.index)
return {"FINISHED"}
@@ -125,8 +131,7 @@ class AddClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
clash_set = context.scene.BIMClashProperties.active_clash_set
source = getattr(clash_set, self.group).add()
return {"FINISHED"}
@@ -140,74 +145,82 @@ class RemoveClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
clash_set = context.scene.BIMClashProperties.active_clash_set
getattr(clash_set, self.group).remove(self.index)
return {"FINISHED"}
class SelectClashSource(bpy.types.Operator, ImportHelper):
class SelectClashSource(bpy.types.Operator):
bl_idname = "bim.select_clash_source"
bl_label = "Select Clash Source"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC file to add as a clash source"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
index: bpy.props.IntProperty()
group: bpy.props.StringProperty()
filename_ext = ".ifc"
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
clash_set = context.scene.BIMClashProperties.active_clash_set
getattr(clash_set, self.group)[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectClashResults(bpy.types.Operator, ImportHelper):
class SelectClashResults(bpy.types.Operator):
bl_idname = "bim.select_clash_results"
bl_label = "Select Clash Results"
bl_description = "Select filepath for clash results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = tool.Clash.get_clash_props()
props.clash_results_path = self.filepath
context.scene.BIMClashProperties.clash_results_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
class SelectSmartGroupedClashesPath(bpy.types.Operator):
bl_idname = "bim.select_smart_grouped_clashes_path"
bl_label = "Select Smart-Grouped Clashes Path"
bl_description = "Select filepath for smart-grouped clashes."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = tool.Clash.get_clash_props()
props.smart_grouped_clashes_path = self.filepath
context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
class ExecuteIfcClash(bpy.types.Operator):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
if self.filepath:
return self.execute(context)
return ExportHelper.invoke(self, context, event)
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
if extension != ".json":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
from ifcclash import ifcclash
self.props = tool.Clash.get_clash_props()
self.props = context.scene.BIMClashProperties
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
@@ -216,7 +229,6 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
self.props.export_path = self.filepath
settings = ifcclash.ClashSettings()
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
@@ -257,7 +269,7 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
context.scene.render.resolution_x = 480
context.scene.render.resolution_y = 270
context.scene.render.image_settings.file_format = "PNG"
context.scene.render.filepath = tool.Blender.get_data_dir_path("shapshot.png").__str__()
context.scene.render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
bpy.ops.render.opengl(write_still=True)
with open(context.scene.render.filepath, "rb") as f:
return ("snapshot.png", f.read())
@@ -275,27 +287,27 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
return {"FINISHED"}
class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
class SelectIfcClashResults(bpy.types.Operator):
bl_idname = "bim.select_ifc_clash_results"
bl_label = "Select IFC Clash Results"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select the clashing IFC geometry stored in a file"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
return ImportHelper.invoke(self, context, event)
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
# TODO refactor into new clash results system
self.file = tool.Ifc.get()
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets = json.load(f)
clash_props = tool.Clash.get_clash_props()
assert clash_props.active_clash_set
clash_set_name = clash_props.active_clash_set.name
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
global_ids = []
for clash_set in clash_sets:
if clash_set["name"] != clash_set_name:
@@ -307,15 +319,13 @@ class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
global_ids.extend([clash["a_global_id"], clash["b_global_id"]])
for obj in context.visible_objects:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
ifc_file = ""
for scene in obj.users_scene:
bim_props = tool.Blender.get_bim_props(scene)
if bim_props.ifc_file:
ifc_file = bim_props.ifc_file
if scene.BIMProperties.ifc_file:
ifc_file = scene.BIMProperties.ifc_file
if scene.library:
break
@@ -327,7 +337,7 @@ class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
element_file = self.file
try:
element = element_file.by_id(props.ifc_definition_id)
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
except:
continue
@@ -347,7 +357,7 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
self.props = tool.Clash.get_clash_props()
self.props = context.scene.BIMClashProperties
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
@@ -381,15 +391,13 @@ class SmartClashGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Clash.get_clash_props()
return bool(props.clash_results_path)
return context.scene.BIMClashProperties.clash_results_path
def execute(self, context):
from ifcclash import ifcclash
settings = ifcclash.ClashSettings()
props = tool.Clash.get_clash_props()
self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json")
self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json")
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
settings.logger.setLevel(logging.DEBUG)
@@ -399,18 +407,19 @@ class SmartClashGroup(bpy.types.Operator):
clash_sets = json.load(f)
# execute the smart grouping
save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance)
save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(
clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance
)
# save smart_groups to json
with open(save_path, "w") as f:
f.write(json.dumps(smart_grouped_clashes))
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
# Reset the list of smart_clash_groups for the UI
props.smart_clash_groups.clear()
context.scene.BIMClashProperties.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -418,7 +427,7 @@ class SmartClashGroup(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = props.smart_clash_groups.add()
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
@@ -436,21 +445,18 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Clash.get_clash_props()
return bool(props.active_clash_set)
return context.scene.BIMClashProperties.active_clash_set
def execute(self, context):
props = tool.Clash.get_clash_props()
smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
with open(smart_groups_path) as f:
smart_grouped_clashes = json.load(f)
# Reset the list of smart_clash_groups for the UI
props.smart_clash_groups.clear()
context.scene.BIMClashProperties.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -458,12 +464,12 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = props.smart_clash_groups.add()
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
for guid in pair:
new_global_id = new_group.global_ids.add()
new_global_id.name = guid
new_global_id.guid = guid
return {"FINISHED"}
@@ -475,19 +481,15 @@ class SelectSmartGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Clash.get_clash_props()
return tool.Ifc.get() and context.visible_objects and props.active_smart_group
return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
def execute(self, context):
ifc_file = tool.Ifc.get()
props = tool.Clash.get_clash_props()
selected_smart_group = props.active_smart_group
assert selected_smart_group
products: list[ifcopenshell.entity_instance] = []
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
products = []
for global_id in selected_smart_group.global_ids:
try:
products.append(ifc_file.by_guid(global_id.name))
except RuntimeError:
products.append(tool.Ifc.get().by_guid(global_id.guid))
except:
continue
tool.Spatial.select_products(products, unhide=True)
context_override = tool.Blender.get_viewport_context()
+10 -58
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup
from bpy.types import PropertyGroup
from bpy.props import (
@@ -30,8 +29,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from mathutils import Vector
from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup):
@@ -46,10 +43,6 @@ class ClashSource(PropertyGroup):
name="Mode",
)
if TYPE_CHECKING:
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
class Clash(PropertyGroup):
a_global_id: StringProperty(name="A")
@@ -58,15 +51,9 @@ class Clash(PropertyGroup):
b_name: StringProperty(name="B Name")
status: BoolProperty(name="Status", default=False)
if TYPE_CHECKING:
a_global_id: str
b_global_id: str
a_name: str
b_name: str
status: bool
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
mode: EnumProperty(
items=[
(
@@ -89,25 +76,11 @@ class ClashSet(PropertyGroup):
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
if TYPE_CHECKING:
mode: Literal["intersection", "collision", "clearance"]
tolerance: float
clearance: float
allow_touching: bool
check_all: bool
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
class SmartClashGroup(PropertyGroup):
number: StringProperty(name="Number")
global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty)
if TYPE_CHECKING:
number: str
global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
class BIMClashProperties(PropertyGroup):
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
@@ -128,39 +101,18 @@ class BIMClashProperties(PropertyGroup):
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
active_clash_text: StringProperty(name="Active Clash Text")
export_path: StringProperty(
name="Export Path",
description=".bcf or .json file to export the clash results to",
subtype="FILE_PATH",
)
if TYPE_CHECKING:
blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty]
blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty]
clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet]
should_create_clash_snapshots: bool
clash_results_path: str
smart_grouped_clashes_path: str
active_clash_set_index: int
active_clash_index: int
smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup]
active_smart_group_index: int
smart_clash_grouping_max_distance: int
p1: Vector
p2: Vector
active_clash_text: str
export_path: str
@property
def active_clash_set(self) -> Union[ClashSet, None]:
return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index)
def active_clash_set(self):
if self.active_clash_set_index < len(self.clash_sets):
return self.clash_sets[self.active_clash_set_index]
@property
def active_smart_group(self) -> Union[SmartClashGroup, None]:
return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index)
def active_smart_group(self):
if self.active_smart_group_index < len(self.smart_clash_groups):
return self.smart_clash_groups[self.active_smart_group_index]
@property
def active_clash(self) -> Union[Clash, None]:
if not (clash_set := self.active_clash_set):
return None
return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index)
def active_clash(self):
if self.active_clash_index < len(self.active_clash_set.clashes):
return self.active_clash_set.clashes[self.active_clash_index]
+5 -8
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.module.clash.data import ClashData
@@ -37,7 +36,9 @@ class BIM_PT_ifcclash(Panel):
ClashData.load()
layout = self.layout
props = tool.Clash.get_clash_props()
scene = context.scene
props = scene.BIMClashProperties
row = layout.row(align=True)
row.operator("bim.add_clash_set")
@@ -117,12 +118,8 @@ class BIM_PT_ifcclash(Panel):
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
layout.prop(props, "export_path")
row = layout.row()
op = row.operator("bim.execute_ifc_clash")
op.filepath = props.export_path
row.operator("bim.execute_ifc_clash")
row = layout.row()
row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
@@ -156,7 +153,7 @@ class BIM_PT_smart_clash_manager(Panel):
def draw(self, context):
layout = self.layout
props = tool.Clash.get_clash_props()
props = context.scene.BIMClashProperties
row = layout.row()
layout.label(text="Select clash results to group:")
@@ -118,7 +118,7 @@ class MaterialClassificationsData(ReferencesData):
def references(cls):
results = []
props = tool.Material.get_material_props()
props = bpy.context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
@@ -145,8 +145,11 @@ class CostClassificationsData(ReferencesData):
@classmethod
def references(cls):
results = []
props = tool.Cost.get_cost_props()
element = tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
element = tool.Ifc.get().by_id(
bpy.context.scene.BIMCostProperties.cost_items[
bpy.context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
@@ -20,21 +20,23 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.classification
import ifcopenshell.util.classification
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.bim.helper
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.ifc import IfcStore
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_classification_library"
bl_label = "Load Classification Library"
bl_description = "Load classification library from the provided filepath."
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
IfcStore.classification_file = ifcopenshell.open(self.filepath)
@@ -198,8 +200,7 @@ class EnableEditingClassification(bpy.types.Operator):
def execute(self, context):
def callback(name, prop, data):
if name == "ReferenceTokens":
geo_props = tool.Georeference.get_georeference_props()
new = geo_props.projected_crs.add()
new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
new.name = name
new.data_type = "string"
new.is_null = data[name] is None
@@ -237,7 +238,7 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
classification: bpy.props.IntProperty()
def _execute(self, context):
self.file = tool.Ifc.get()
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"classification.remove_classification",
tool.Ifc.get(),
@@ -252,18 +253,19 @@ class EditClassification(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMClassificationProperties
def callback(attributes, prop):
if prop.name == "ReferenceTokens":
attributes[prop.name] = json.loads(prop.string_value)
return True
attributes = bonsai.bim.helper.export_attributes(props.classification_attributes, callback=callback)
ifc_file = tool.Ifc.get()
ifcopenshell.api.classification.edit_classification(
ifc_file,
classification=ifc_file.by_id(props.active_classification_id),
attributes=attributes,
attributes = {}
for attribute in props.classification_attributes:
if attribute.is_null:
attributes[attribute.name] = None
elif attribute.name == "ReferenceTokens":
attributes[attribute.name] = json.loads(attribute.string_value)
else:
attributes[attribute.name] = attribute.string_value
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"classification.edit_classification",
self.file,
**{"classification": self.file.by_id(props.active_classification_id), "attributes": attributes},
)
bpy.ops.bim.disable_editing_classification()
@@ -344,11 +346,17 @@ class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMClassificationReferenceProperties
attributes = bonsai.bim.helper.export_attributes(props.reference_attributes)
ifc_file = tool.Ifc.get()
ifcopenshell.api.classification.edit_reference(
ifc_file,
reference=ifc_file.by_id(props.active_reference_id),
attributes = {}
for attribute in props.reference_attributes:
if attribute.is_null:
attributes[attribute.name] = None
else:
attributes[attribute.name] = attribute.string_value
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"classification.edit_reference",
self.file,
reference=self.file.by_id(props.active_reference_id),
attributes=attributes,
)
bpy.ops.bim.disable_editing_classification_reference()
@@ -457,7 +465,6 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
for prop in blender_properties:
properties[prop.name] = prop.get_value()
# TODO: is this still the correct approach?
if classification_pset.name == "undefined_set":
if "ObjectType" in properties:
if hasattr(element, "ObjectType"):

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