Compare commits

..

29 Commits

Author SHA1 Message Date
Andrej730 a8ba1d1361 build_osx - separate arm64 ccache to avoid override 2025-09-26 19:36:12 +05:00
Andrej730 618e5e60d9 build_osx - fix issue building arm64 2025-09-26 19:34:32 +05:00
Andrej730 efa881faa0 build-all - bump Python version to fix issue cross compiling
Not sure which patch version exactly fixed it, but 3.13.6 works.
2025-09-26 19:31:49 +05:00
Andrej730 18a26672e9 cmake - temporarily pin git_sha to reuse ccache for debugging 2025-09-26 19:31:48 +05:00
Andrej730 49e9aa1652 build_osx - add deps for cross compilation 2025-09-26 19:31:48 +05:00
Andrej730 07758c29ad build_osx - exclude arm64 gmp dependency to avoid cross compilation issues 2025-09-26 19:31:48 +05:00
Andrej730 0be6f897b8 build-all - cross compile dependencies 2025-09-26 19:31:48 +05:00
Andrej730 6db3728f0e build-all.py - provide with-openssl for python on Mac 2025-09-26 19:31:48 +05:00
Andrej730 879dc52865 build_osx - note on preinstalled dependencies 2025-09-26 19:31:48 +05:00
Andrej730 640deb0188 build-all.py - document ADD_COMMIT_SHA env var 2025-09-26 19:31:47 +05:00
Andrej730 6989e509ce cmake - don't fail silently with ADD_COMMIT_SHA and missing git 2025-09-26 19:31:47 +05:00
Andrej730 efd79ef079 build_osx - attempt to cross-compile intel binaries 2025-09-26 19:31:47 +05:00
Andrej730 356d2bb753 build-all.py - override minimum cmake version for OpenCOLLADA 2025-09-26 19:31:47 +05:00
Andrej730 51b76237fe build-all.py - bump eigen package to avoid issues on cmake 4 2025-09-26 19:31:47 +05:00
Andrej730 58db9dba0a workflows - unify ccache names 2025-09-26 19:31:47 +05:00
Andrej730 5e460898f8 build-all.py - bump json package to avoid issues on cmake 4 2025-09-26 19:31:46 +05:00
Andrej730 c86b5bc773 build_osx - upload logs artifacts to help debugging 2025-09-26 19:31:46 +05:00
Andrej730 a55b5ba21f build-rocky - upload logs artifacts to help debugging 2025-09-26 19:31:46 +05:00
Andrej730 bb4b7dae52 build-all.py - opencollada patch to support config package on Unix 2025-09-26 19:31:46 +05:00
Andrej730 2f4f7d6374 build-all.py - add OpenCOLLADA to IfcGeom dependencies
Otherwise it can be overlooked if you build IfcOpenShell-Python or any other target specifically, it was only built if you run build-all.py without any target arguments.
2025-09-26 19:31:46 +05:00
Andrej730 cbb5211888 build-all.py - CGAL to use v5.6.3
5.6.x-branch was removed
2025-09-26 19:31:46 +05:00
Andrej730 a65a50d5d6 build-all.py - alter rocksdb's deps search only on Windows (da4508e) 2025-09-26 19:31:46 +05:00
Andrej730 9d0d51337b build-all.py - add a note about xz requirement 2025-09-26 19:31:46 +05:00
Andrej730 e9cdd55c96 build-all.py - fix Python 3.6 support for rocky (566c9af) 2025-09-26 19:31:45 +05:00
Andrej730 0ca3c0c806 ruff ci - check python 3.7 compatibility for build-all.py 2025-09-26 19:31:45 +05:00
Andrej730 ef05f2b3f6 build-all.py - print all missing commands simultaneoously 2025-09-26 19:31:45 +05:00
Andrej730 11f0a8d434 Fix missing yacc in linux builds 2025-09-26 19:31:45 +05:00
Andrej730 3f316a99f1 black . 2025-09-26 19:31:45 +05:00
Andrej730 59b5e200a5 typing 2025-09-26 19:31:44 +05:00
281 changed files with 3231 additions and 9358 deletions
+12 -8
View File
@@ -14,6 +14,10 @@ jobs:
runner: macos-14
arch: x64
oldarch:
- os: macos
runner: macos-14
arch: arm64
oldarch: m1
steps:
- name: Checkout Repository
@@ -30,6 +34,11 @@ jobs:
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
@@ -43,12 +52,11 @@ jobs:
- name: Unpack Dependencies
run: |
rm -rf ./build/*/*/*/install/*gmp* ./build/*/*/*/install/*cgal*
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1
with:
key: mac-${{ matrix.arch }}
@@ -64,16 +72,12 @@ jobs:
# We don't use gmpcxx, but it comes preinstalled on macos runner as arm64 bottle
# and CGAL detects it and breaks cross compilation.
brew uninstall --ignore-dependencies gmp
# Otherwise Python will fallback to use arm64 `pkg-config`,
# will pick up arm64 libraries ('zstd' in particular),
# and break the build.
/usr/local/bin/brew install pkg-config
# Required by Python.
/usr/local/bin/brew install gettext openssl
fi
set -o pipefail
IFCOS_SCHEMAS=4 CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
python3 ./nix/build-all.py -v -py-313 --diskcleanup ${MAC_INTEL} \
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
python3 ./nix/build-all.py -v --diskcleanup ${MAC_INTEL} \
| tee build.log
- name: Upload Build Logs
+21 -55
View File
@@ -14,67 +14,33 @@ jobs:
submodules: recursive
path: IfcOpenShell
- name: Checkout Build Repository
- name: Checkout Pyodide
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ifcopenshell_build
ref: wasm
lfs: true
submodules: recursive
repository: pyodide/pyodide
ref: '0.28.0a3'
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Unpack Dependencies
run: |
cd ifcopenshell_build
python ../IfcOpenShell/pyodide/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ubuntu-22.04-${{ runner.arch }}
path: pyodide
- name: Build
run: |
./IfcOpenShell/pyodide/build_pyodide.sh
FILE=`echo dist/ifcopenshell-*.whl`
NEW_FILE=`echo $FILE | sed "s/-/+${GITHUB_SHA:0:7}-/2"`
mv $FILE $NEW_FILE
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: build-logs-pyodide
path: |
ifcopenshell_build/*/*/logs/*.log
retention-days: 30
- name: Run wheel tests
run: |
cp -r IfcOpenShell/pyodide/test test
# venv set up in build_pyodide.sh.
source .venv/bin/activate
uv pip install pytest-pyodide
PYODIDE_ROOT_DIST=`pyodide config get pyodide_root`/dist
# `pytest-pyodide` requires pyodide in 'pyodide' directory in cwd, when running `pytest`.
cp -r $PYODIDE_ROOT_DIST test/pyodide
cp dist/ifcopenshell-*.whl test/pyodide
cd test
pytest --capture=no
- name: Pack Dependencies
run: |
cd ifcopenshell_build
python ../IfcOpenShell/pyodide/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
cd ifcopenshell_build
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add */*/install/cache-*.tar.gz
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || echo "Push failed"
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' >> 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/22.16.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}-cp313-cp313-emscripten_4_0_9_wasm32.whl
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
+5 -5
View File
@@ -47,11 +47,11 @@ jobs:
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
- name: ccache
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
- name: Run Build Script
shell: bash
+5 -5
View File
@@ -47,11 +47,11 @@ jobs:
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
- name: ccache
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
- name: Run Build Script
shell: bash
+49 -11
View File
@@ -9,6 +9,7 @@ jobs:
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
@@ -29,6 +30,14 @@ jobs:
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-vs2022-x64-installed
@@ -36,21 +45,21 @@ jobs:
7z x $_.FullName
}
- name: ccache
# Use fork to resolve cache misses / duplicated cache entries on Windows.
uses: Andrej730/ccache-action@main
with:
key: win-${{ matrix.arch }}
# Windows ccache needs ~1GB
# and with default 500MB some cache gets deleted, leading to misses.
max-size: 5000MB
- name: Run Build Script And Pack .zip Archives
- 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\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
SET IFCOS_INSTALL_PYTHON=FALSE
cd win
python build-all-win.py
echo y | call build-deps.cmd vs2022-x64 Release
SET PYTHONHOME=C:\Python\${{ matrix.python }}
call run-cmake.bat vs2022-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
call install-ifcopenshell.bat vs2022-x64 Release
- name: Pack Dependencies
run: |
@@ -72,6 +81,35 @@ jobs:
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-vs2022-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:
+4 -6
View File
@@ -26,7 +26,6 @@ jobs:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install ruff
uv tool install black
uv tool install poethepoet
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
@@ -41,16 +40,15 @@ jobs:
- name: Black formatter
id: black
run: |
black --diff --check .
uvx black --diff --check .
continue-on-error: true
- name: Ruff check
id: ruff
run: |
ERROR=0
poe ruff-main || ERROR=1
poe ruff-old || ERROR=1
exit $ERROR
uvx ruff check
# It's actually Python 3.6, but ruff only supports 3.7+, but it should do.
uvx ruff check nix/build-all.py --target-version py37
continue-on-error: true
- name: Final check
@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312, py313, py314]
pyver: [py39, py310, py311, py312, py313]
config:
- {
name: "Windows 64bit",
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312, py313, py314]
pyver: [py39, py310, py311, py312, py313]
config:
- {
name: "Windows 64bit",
-32
View File
@@ -1,32 +0,0 @@
name: ci-ifctester-org
on:
workflow_dispatch:
push:
paths:
- src/ifctester/**
jobs:
publish_website:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Checkout ifctester_org_static_html
uses: actions/checkout@v4
with:
repository: IfcOpenShell/ifctester_org_static_html
token: ${{ secrets.IFCOPENBOT_TOKEN }}
path: ifctester_org_static_html
- name: Build webapp
working-directory: ./src/ifctester
run: |
sudo apt update && sudo apt install -y nodejs
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
make webapp-build
- name: Commit and push
run: |
cp -r src/ifctester/webapp/dist/* ifctester_org_static_html/
git -C ifctester_org_static_html add .
git -C ifctester_org_static_html commit --allow-empty -m "$(git log --oneline -1)"
git -C ifctester_org_static_html push
+28 -27
View File
@@ -49,8 +49,7 @@ jobs:
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install src/bcf --no-deps
pip install git+https://github.com/zdhoward/aud
pip install pytest-xdist==3.8.0
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
- name: Install C++ dependencies
run: |
@@ -70,19 +69,16 @@ jobs:
libhdf5-dev libcgal-dev libeigen3-dev
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1
with:
key: ubuntu-22.04-${{ runner.arch }}
# RTTI is only enabled by default in Debug builds of rocksdb.
# Distros are using Release builds, so we're compiling it ourselves with RTTI forced on.
# https://github.com/facebook/rocksdb/blob/a3aa44a7167b8336f9bc15c8aba063260268ff68/CMakeLists.txt#L433
# rocksdb on debian distros misses RTTI?
- name: build rocksdb
run: |
git clone https://github.com/facebook/rocksdb --branch v9.11.2
cd rocksdb
mkdir build && cd build
# rocksdb is using ccache automatically.
cmake -DFAIL_ON_WARNINGS=Off \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr/local \
@@ -103,9 +99,9 @@ jobs:
echo ${{ env.pythonLocation }}
mkdir build && cd build
# Ubuntu 22.04's libocct-foundation-dev package doesn't have Config.cmake, so we provide OCC paths directly.
# In later versions of Ubuntu, this can be simplified and the OCC paths can be removed.
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
@@ -114,32 +110,37 @@ jobs:
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
-DCOLLADA_SUPPORT=Off \
-DUSE_MMAP=On \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
-DGMP_INCLUDE_DIR=/usr/include \
-DMPFR_INCLUDE_DIR=/usr/include \
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
-DEIGEN_DIR=/usr/include/eigen3 \
-DWITH_ROCKSDB=On \
../cmake
sudo make -j $(nproc)
sudo make install
# - name: Run IfcConvert on Sample files
# run: |
# (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
# echo $i | tee -a log; \
# timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
# echo $i $? >> statuses; \
# done) || true
# echo Failed
# grep -v 0$ statuses
# grep -v 0$ statuses | wc -l
# echo Succeeded
# grep 0$ statuses
# grep 0$ statuses | wc -l
- name: Run IfcConvert on Sample file
- name: Run IfcConvert on Sample files
run: |
IfcConvert test/input/acad2010_walls.ifc test/input/acad2010_walls.obj
(find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
echo $i | tee -a log; \
timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
echo $i $? >> statuses; \
done) || true
echo Failed
grep -v 0$ statuses
grep -v 0$ statuses | wc -l
echo Succeeded
grep 0$ statuses
grep 0$ statuses | wc -l
- name: Test ifcopenshell-python
run: |
@@ -149,7 +150,7 @@ jobs:
mv ifcopenshell ifcopenshell-local # Force testing on installed module
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
ERROR=0
make test-parallel || ERROR=1
make test || ERROR=1
cd ../bcf && make test || ERROR=1
pip install requests
cd ../bsdd && make test || ERROR=1
+1 -4
View File
@@ -109,7 +109,4 @@ src/bonsai/bonsai/bim/schema/Brick.ttl
bonsaiDecoratorForLoads.code-workspace
dev_environment.bat
.pixi/
src/ifcopenshell-python/ifcopenshell/express/*.exp
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
.pixi/
+18 -18
View File
@@ -37,25 +37,25 @@ Contents
| Name | Description | License | Service |
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
| [bcf](https://docs.ifcopenshell.org/bcf.html) | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/bcf-client/badges/version.svg)](https://anaconda.org/conda-forge/bcf-client) |
| [bonsai](https://docs.ifcopenshell.org/bonsai.html) | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| [bsdd](https://docs.ifcopenshell.org/bsdd.html) | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) |
| [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| [ifc4d](https://docs.ifcopenshell.org/ifc4d.html) | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) |
| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc5d?label=PyPI&color=006dad)](https://pypi.org/project/ifc5d/) |
| [ifcbimtester](https://docs.ifcopenshell.org/bimtester.html) | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/bcf-client/badges/version.svg)](https://anaconda.org/conda-forge/bcf-client) |
| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) |
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) |
| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc5d?label=PyPI&color=006dad)](https://pypi.org/project/ifc5d/) |
| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccityjson?label=PyPI&color=006dad)](https://pypi.org/project/ifccityjson/) |
| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcclash?label=PyPI&color=006dad)](https://pypi.org/project/ifcclash/) |
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) |
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccityjson?label=PyPI&color=006dad)](https://pypi.org/project/ifccityjson/) |
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcclash?label=PyPI&color=006dad)](https://pypi.org/project/ifcclash/) |
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) |
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) |
| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
The IfcOpenShell C++ codebase is split into multiple interal libraries:
+44 -73
View File
@@ -27,11 +27,13 @@ endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
endif()
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
if(POLICY CMP0141) # 3.25+
cmake_policy(SET CMP0141 NEW) # Support for `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT`.
endif()
if(POLICY CMP0144) # 3.27
cmake_policy(SET CMP0144 NEW) # find_package() uses upper-case <PACKAGENAME>_ROOT variables.
endif()
@@ -85,8 +87,8 @@ option(WITH_ROCKSDB "Support a RocksDB key-value store as a file backend in IfcO
option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
option(VERSION_OVERRIDE "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" OFF)
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, warning results in many rebuilds, requires git" OFF)
option(VERSION_OVERRIDE "Override the version defined in IfcParse.h with the file VERSION in the repository root" OFF)
set(
PYTHON_MODULE_INSTALL_DIR
@@ -121,28 +123,6 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
set(BUILD_IFCGEOM ON)
endif()
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
# By default Visual Studio generators will use /Zi which is not compatible
# with ccache, so tell Visual Studio to use /Z7 instead.
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
# Not needed for Ninja.
if(CMAKE_GENERATOR MATCHES "Visual Studio")
file(COPY_FILE
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS
"CLToolExe=cl.exe"
"CLToolPath=${CMAKE_BINARY_DIR}"
"UseMultiToolTask=true"
)
endif()
endif()
endif()
if(MSVC AND MSVC_PARALLEL_BUILD)
add_definitions("/MP")
endif()
@@ -208,8 +188,6 @@ if(NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(PYTHON_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(PYTHON_LIBRARY)
UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE)
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
@@ -252,13 +230,6 @@ endmacro()
if(WITH_CGAL)
if(NOT CGAL_INCLUDE_DIR)
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
# and sometiems it's getting in the way.
if(NOT DEFINED Boost_USE_STATIC_LIBS)
set(CGAL_Boost_USE_STATIC_LIBS OFF)
else()
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
endif()
find_package(CGAL REQUIRED)
if(NOT CGAL_DIR)
message(
@@ -426,7 +397,7 @@ if(WASM_BUILD)
else()
# @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features?
set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
set(BOOST_COMPONENTS system program_options regex thread date_time)
endif()
if(USE_MMAP)
@@ -451,11 +422,8 @@ if(NOT MINIMAL_BUILD)
clear_wasm_sysroot()
if(IFCXML_SUPPORT)
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
# CONFIG is provided using root path, so no need to clear sysroot here.
restore_wasm_sysroot()
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04?)
find_package(LibXml2 QUIET CONFIG)
clear_wasm_sysroot()
if(NOT LibXml2_FOUND)
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
@@ -794,7 +762,7 @@ if(HDF5_SUPPORT)
endif()
endif()
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
if(NOT HDF5_INCLUDE_DIR)
# First try to find it as a config.
find_package(HDF5 CONFIG)
if(HDF5_DIR)
@@ -804,11 +772,10 @@ if(HDF5_SUPPORT)
# If it failed, still try to find as a module.
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
find_package(HDF5 COMPONENTS CXX)
find_package(HDF5)
if(NOT HDF5_INCLUDE_DIR)
message(
FATAL_ERROR
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
"Also could not find HDF5 package (neither module or config)."
)
@@ -1044,7 +1011,11 @@ if(BUILD_IFCGEOM)
foreach(schema ${SCHEMA_VERSIONS})
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema})
endforeach()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
if(WASM_BUILD)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
else()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
endif()
endif()
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
@@ -1058,14 +1029,12 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
set(GEOM_SERIALIZER_SCHEMA_LIBRARIES ${GEOM_SERIALIZER_SCHEMA_LIBRARIES} GeometrySerializers_ifc${schema})
add_library(geometry_serializer_ifc${schema} STATIC ../src/ifcgeom/Serialization/schema/Serialization.cpp)
set_target_properties(geometry_serializer_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS -DIfcSchema=Ifc${schema}")
target_link_libraries(geometry_serializer_ifc${schema} ${OPENCASCADE_LIBRARIES})
set_target_properties(geometry_serializer_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
list(APPEND geometry_serializer_libraries geometry_serializer_ifc${schema})
endforeach()
add_library(geometry_serializer STATIC ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
set_target_properties(geometry_serializer PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS")
target_link_libraries(geometry_serializer ${geometry_serializer_libraries} IfcParse)
add_library(geometry_serializer STATIC ../src/ifcgeom/Serialization/Serialization.cpp)
target_link_libraries(geometry_serializer ${geometry_serializer_libraries})
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} geometry_serializer ${geometry_serializer_libraries})
endif()
endif()
@@ -1103,6 +1072,13 @@ endforeach()
set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0)
message(STATUS "GCC < 9 detected, linking stdc++fs explicitly")
set(STDCPPFS stdc++fs)
endif()
endif()
add_library(IfcParse ${IFCPARSE_FILES})
target_link_libraries(IfcParse ${STDCPPFS})
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
@@ -1143,23 +1119,23 @@ if(BUILD_IFCGEOM)
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/kernels/${kernel}/*.cpp)
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
add_library(geometry_kernel_${kernel} ${IFCGEOM_FILES} ../src/ifcgeom/kernels/ifc_geomlibrary_api.h)
set_property(TARGET geometry_kernel_${kernel} APPEND PROPERTY COMPILE_FLAGS "-DIFC_GEOMLIBRARY_EXPORTS")
add_library(geometry_kernel_${kernel} ${IFCGEOM_FILES})
set_property(TARGET geometry_kernel_${kernel} APPEND PROPERTY COMPILE_FLAGS "-DIFC_GEOM_EXPORTS")
# needed?
# if(NOT WASM_BUILD)
# endif()
target_link_libraries(geometry_kernel_${kernel} ${${KERNEL_UPPER}_LIBRARIES} IfcGeom IfcParse)
target_link_libraries(geometry_kernel_${kernel} ${${KERNEL_UPPER}_LIBRARIES})
list(APPEND kernel_libraries geometry_kernel_${kernel})
if(${kernel} STREQUAL "cgal")
set_property(TARGET geometry_kernel_${kernel} APPEND_STRING PROPERTY COMPILE_FLAGS " -DCGAL_HAS_THREADS")
add_library(geometry_kernel_${kernel}_simple ${IFCGEOM_FILES})
set_target_properties(geometry_kernel_${kernel}_simple PROPERTIES COMPILE_FLAGS "-DIFC_GEOMLIBRARY_EXPORTS -DIFOPSH_SIMPLE_KERNEL -DCGAL_HAS_THREADS")
set_target_properties(geometry_kernel_${kernel}_simple PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIFOPSH_SIMPLE_KERNEL -DCGAL_HAS_THREADS")
# needed?
# if(NOT WASM_BUILD)
# endif()
target_link_libraries(geometry_kernel_${kernel}_simple ${${KERNEL_UPPER}_LIBRARIES} IfcGeom IfcParse)
target_link_libraries(geometry_kernel_${kernel}_simple ${${KERNEL_UPPER}_LIBRARIES})
list(APPEND kernel_libraries geometry_kernel_${kernel}_simple)
endif()
endforeach()
@@ -1174,9 +1150,6 @@ if(BUILD_IFCGEOM)
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
if (NOT BUILD_SHARED_LIBS)
target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
endif()
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
endforeach()
@@ -1192,7 +1165,11 @@ if(BUILD_IFCGEOM)
find_package(Threads)
endif()
target_link_libraries(IfcGeom IfcParse ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT})
if(WASM_BUILD)
target_link_libraries(IfcGeom ${kernel_libraries} ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT})
else()
target_link_libraries(IfcGeom IfcParse ${kernel_libraries} ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT})
endif()
endif(BUILD_IFCGEOM)
@@ -1207,7 +1184,7 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
foreach(schema ${SCHEMA_VERSIONS})
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DSERIALIZERS_EXPORTS -DIfcSchema=Ifc${schema}")
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if(WASM_BUILD)
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
@@ -1217,7 +1194,7 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
endforeach()
add_library(Serializers ${SERIALIZERS_FILES})
set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DSERIALIZERS_EXPORTS" VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS" VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
if(WITH_PROJ)
target_compile_definitions(Serializers PRIVATE "WITH_PROJ")
@@ -1228,7 +1205,7 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
target_link_libraries(Serializers ${PROJ_LIBRARIES})
endif()
target_link_libraries(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES} IfcGeom ${OPENCASCADE_LIBRARIES} ${kernel_libraries} IfcParse)
target_link_libraries(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES})
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
@@ -1275,7 +1252,7 @@ if(BUILD_CONVERT)
set(IFCCONVERT_FILES ${IFCCONVERT_CPP_FILES} ${IFCCONVERT_H_FILES})
add_executable(IfcConvert ${IFCCONVERT_FILES})
target_link_libraries(IfcConvert IfcGeom IfcParse Serializers ${kernel_libraries} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES} ${HDF5_LIBRARIES} ${USD_LIBRARIES})
target_link_libraries(IfcConvert ${IFCOPENSHELL_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES} ${HDF5_LIBRARIES} ${USD_LIBRARIES})
if (WITH_RELATIONSHIP_VALIDATION)
set_property(TARGET IfcConvert APPEND_STRING PROPERTY COMPILE_FLAGS " -DWITH_RELATIONSHIP_VALIDATION")
endif()
@@ -1309,7 +1286,7 @@ if(BUILD_GEOMSERVER)
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
set(SOURCE_FILES ${CPP_FILES} ${H_FILES})
add_executable(IfcGeomServer ${SOURCE_FILES})
target_link_libraries(IfcGeomServer IfcGeom IfcParse Serializers ${kernel_libraries} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES})
target_link_libraries(IfcGeomServer ${IFCOPENSHELL_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES})
if((NOT WIN32) AND BUILD_SHARED_LIBS)
SET_INSTALL_RPATHS(IfcGeomServer "${IFCOPENSHELL_LIBRARY_DIR};${OCC_LIBRARY_DIR};${Boost_LIBRARY_DIRS}")
@@ -1359,6 +1336,7 @@ if(ADD_COMMIT_SHA)
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(git_sha "418eb277d")
message(STATUS "IfcOpenShell branch: \"${git_branch}\"")
message(STATUS "IfcOpenShell commit: \"${git_sha}\"")
@@ -1366,10 +1344,8 @@ if(ADD_COMMIT_SHA)
message(FATAL_ERROR "Unable to determine commit sha and/or branch")
endif()
target_compile_definitions(IfcParse PRIVATE
-DIFCOPENSHELL_BRANCH=${git_branch}
-DIFCOPENSHELL_COMMIT=${git_sha}
)
add_definitions(-DIFCOPENSHELL_BRANCH=${git_branch})
add_definitions(-DIFCOPENSHELL_COMMIT=${git_sha})
endif()
endif(ADD_COMMIT_SHA)
@@ -1426,11 +1402,6 @@ if(BUILD_IFCGEOM)
DESTINATION ${INCLUDEDIR}/ifcgeom
)
file(GLOB SERIALIZATION_H_FILES ../src/ifcgeom/serialization/*.h)
install(FILES ${SERIALIZATION_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom/serialization
)
foreach(kernel ${GEOMETRY_KERNELS})
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/kernels/${kernel}/*.h)
install(FILES ${IFCGEOM_H_FILES}
+97 -220
View File
@@ -31,7 +31,6 @@ Available arguments:
``-py-313`` - build for specific Python version
(building for all supported Python version by default).
``-wasm`` - compile for wasm
``-without-xxx`` - do not build dependency ``xxx`` (e.g. ``--without-swig``)
``-mac-cross-compile-intel`` - cross compile for Intel Mac on Apple Silicon host
``-shared`` - build shared libraries. By default will build static.
``-diskcleanup`` - clean up build directories after finishing building dependencies
@@ -51,13 +50,6 @@ Used environment variables:
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (IFC2X3; IFC4; IFC4X3_ADD2) - to be supplied as `2x3;4`
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
(`true` by default, any other value is considered `false`)
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
used to deduce `PYVERSION` (e.g. '3.13.2'), `PYTHONINCLUDE`,
`SIDE_MODULE_CFLAGS`, `SIDE_MODULE_LDFLAGS`.
Allows to build wasm without pyodide build environment, which can be useful for debugging build issues.
Example value: 'pyodide/cpython/installs/python-3.13.2'
- ``WASM_TOOLCHAIN_FILE`` - path to emscripten toolchain file from pyodide ('Emscripten.cmake')
needed only if ``WASM_PYTHON_PATH`` is provided.
- ``ADD_COMMIT_SHA`` - if defined with any non-empty value then
`ADD_COMMIT_SHA` and `VERSION_OVERRIDE` will be set to `ON` while configuring IfcOpenShell
@@ -141,7 +133,7 @@ PROJECT_NAME = "IfcOpenShell"
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6"]
JSON_VERSION = "3.11.3"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.8.1"
@@ -154,7 +146,7 @@ SWIG_VERSION = "4.1.0"
OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.13.1"
GMP_VERSION = "6.3.0"
GMP_VERSION = "6.2.1"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "v5.6.3"
USD_VERSION = "23.05"
@@ -201,59 +193,19 @@ def cecho(message, color=NO_COLOR):
logger.info(f"{color}{message}\033[0m")
def which(cmd: str) -> Union[str, None]:
PATH = os.getenv("PATH")
assert PATH
for path in PATH.split(":"):
if os.path.exists(path) and cmd in os.listdir(path):
return cmd
return None
# Flags.
APPLE = platform.system() == "Darwin"
MAC_CROSS_COMPILE_INTEL = "mac-cross-compile-intel" in flags
assert platform.system() == "Darwin" or not MAC_CROSS_COMPILE_INTEL
WASM = "wasm" in flags
"""Build WASM outside pyodide build environment."""
WASM_CMAKE_IS_USING_INIT_VARS = False
if WASM:
def get_pyodide_config_var(var_name: str) -> str:
output = sp.check_output(["pyodide", "config", "get", var_name], encoding="utf-8").strip()
return output
if "PYODIDE_ROOT" not in os.environ:
cecho("WARNING. Couldn't find 'PYODIDE_ROOT' in environment variables.", YELLOW)
cecho("Assuming building wasm outside pyodide build environment and resetting necessary variables.", YELLOW)
os.environ["SIDE_MODULE_CFLAGS"] = get_pyodide_config_var("cflags")
os.environ["SIDE_MODULE_LDFLAGS"] = get_pyodide_config_var("ldflags")
# Override cmake toolchain for all `emcmake` calls,
# needed for shared libraries (resulting .so wrapper)
# and to ensure compilation is pyodide compatible (e.g. `-fwasm-exceptions` is used in compilation flags).
os.environ["CMAKE_TOOLCHAIN_FILE"] = get_pyodide_config_var("cmake_toolchain_file")
required_vars = (
"SIDE_MODULE_CFLAGS",
"SIDE_MODULE_LDFLAGS",
"CMAKE_TOOLCHAIN_FILE",
)
missing_vars = [v for v in required_vars if v not in os.environ]
assert not missing_vars, f"Some variables required for WASM compilation are missing: {', '.join(missing_vars)}"
def get_pyodide_build_version() -> "tuple[int, ...]":
pyodide_build_suffix = "pyodide-build version:"
output = sp.check_output(["pyodide", "--version"], encoding="utf-8").strip()
assert pyodide_build_suffix in output, output
version_line = next(l for l in output.splitlines() if l.startswith(pyodide_build_suffix))
version = version_line.partition(":")[2].strip()
return tuple(map(int, version.split(".")))
# Pyodide still in transition from `FLAGS` to `FLAGS_INIT`.
# `FLAGS_INIT` allow us to provide flags using environment variables
# and providing `FLAGS` directly would break pyodide toolchain.
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (0, 30, 8)
# pyodide provide empty `CXXFLAGS`, leading to issues using C++ files compiled with `-fexceptions`
# which is used by OCCT.
# https://github.com/pyodide/pyodide-build/issues/251
side_module_cxx_flags = os.environ.get("SIDE_MODULE_CXXFLAGS", "")
if side_module_cxx_flags.strip():
print("SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').")
print("Maybe it's time to stop overriding them in the script?")
os.environ["SIDE_MODULE_CXXFLAGS"] = os.environ["SIDE_MODULE_CFLAGS"]
# Set defaults for missing empty environment variables
@@ -269,11 +221,9 @@ if platform.system() == "Darwin":
IFCOS_NUM_BUILD_PROCS = os.getenv("IFCOS_NUM_BUILD_PROCS", multiprocessing.cpu_count() + 1)
SCRIPT_PATH = Path(__file__).parent
REPO_PATH = SCRIPT_PATH.parent
CMAKE_DIR = (REPO_PATH / "cmake").resolve().__str__()
CMAKE_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "cmake"))
BUILD_DIR = os.environ.get("BUILD_DIR", (REPO_PATH / "build").__str__())
build_dir = os.environ.get("BUILD_DIR", os.path.join(os.path.dirname(__file__), "..", "build"))
if WASM:
@@ -282,7 +232,7 @@ elif MAC_CROSS_COMPILE_INTEL:
arch = "x86_64"
else:
arch = platform.machine()
DEFAULT_DEPS_DIR = Path(BUILD_DIR) / platform.system() / arch
DEFAULT_DEPS_DIR = Path(build_dir) / platform.system() / arch
if TOOLSET:
DEFAULT_DEPS_DIR = DEFAULT_DEPS_DIR / TOOLSET
@@ -314,7 +264,6 @@ if USE_OCCT:
cecho(" - Compiling against official Open Cascade")
else:
cecho(" - Compiling against Open Cascade Community Edition")
cecho(f"* Build Directory = {BUILD_DIR}", MAGENTA)
cecho(f"* Dependency Directory = {DEPS_DIR}", MAGENTA)
cecho(f" - The directory where {PROJECT_NAME} dependencies are installed.")
cecho(f"* Build Config Type = {BUILD_CFG}", MAGENTA)
@@ -331,11 +280,6 @@ cecho(
""" - How many compiler processes may be run in parallel.
"""
)
cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA)
cecho(
""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
"""
)
dependency_tree: "dict[str, tuple[str, ...]]" = {
"IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"),
@@ -382,13 +326,11 @@ if MAC_CROSS_COMPILE_INTEL:
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = ["architecture=x86"]
MAC_CROSS_COMPILE_INTEL_CXX = "clang++ -arch x86_64"
MAC_CROSS_COMPILE_INTEL_CC = "clang -arch x86_64"
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = ["--host=x86_64-apple-darwin"]
else:
MAC_CROSS_COMPILE_INTEL_ARGS = []
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = []
MAC_CROSS_COMPILE_INTEL_CXX = ""
MAC_CROSS_COMPILE_INTEL_CC = ""
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
OFF_ON = ["OFF", "ON"]
BUILD_STATIC = "shared" not in flags
@@ -402,57 +344,27 @@ PIC = "-fPIC" if BUILD_STATIC else ""
if any(f.startswith("py-") for f in flags):
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if f"py-{''.join(pyv.split('.')[:2])}" in flags]
if any(f.startswith("occt-") for f in flags):
OCCT_VERSION = next(f.split("-", 1)[1] for f in flags if f.startswith("occt-"))
print(OCCT_VERSION)
if explicit_targets:
targets = {dep for target in explicit_targets for dep in gather_dependencies(target)}
else:
targets = set(dependency_tree.keys())
targets = set(t for t in targets if "without-%s" % t.lower() not in flags)
if WASM:
SKIP_TARGETS_FOR_WASM = {
"hdf5",
"rocksdb",
"opencollada",
"swig",
"pcre",
"pcre2",
"IfcGeom",
"IfcConvert",
"IfcGeomServer",
}
SKIP_TARGETS_FOR_WASM = {t.lower() for t in SKIP_TARGETS_FOR_WASM}
skip_targets = {t for t in targets if t.lower() in SKIP_TARGETS_FOR_WASM}
if skip_targets:
cecho(f"Skipping targets for wasm build: {', '.join(sorted(skip_targets))}", YELLOW)
targets.difference_update(skip_targets)
print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t)))))
# Check that required tools are in PATH
yacc = "yacc" # Used during swig building process, installed with `bison` on Debian / `byacc` on Red Hat.
missing_commands: "list[str]" = []
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz]
if "wasm" in flags:
# Skip swig build for WASM.
required_commands.append("swig")
required_commands.append("pyodide")
required_commands.remove(yacc)
for cmd in required_commands:
if shutil.which(cmd) is None:
for cmd in [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz]:
if which(cmd) is None:
missing_commands.append(cmd)
if missing_commands:
raise ValueError(f"Required tools not installed or not added to PATH: {', '.join(missing_commands)}")
MAC_INTEL_BIN_PATH = "/usr/local/bin"
if MAC_CROSS_COMPILE_INTEL:
brew = f"{MAC_INTEL_BIN_PATH}/brew"
brew = "/usr/local/bin/brew"
assert os.path.exists(brew), f"For intel cross compilation the brew path is expected to be '{brew}'."
# identifiers for the download tool (could be less memory consuming as ints, but are more verbose as strings)
@@ -476,13 +388,6 @@ except:
pass
def restore_env(var_name: str, old_value: Union[str, None]) -> None:
if old_value is None:
del os.environ[var_name]
else:
os.environ[var_name] = old_value
def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool = False) -> str:
"""
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
@@ -582,27 +487,16 @@ def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None,
if "wasm" in flags:
wasm.append("emcmake")
cmake_flags: list[str] = []
if not WASM or not WASM_CMAKE_IS_USING_INIT_VARS:
# For WASM we provide flags using just environment variables.
# If we provide them using cmake vars, it will override emscripten toolchain flags.
# Unsure if we need this in general even for non-WASM builds.
cmake_flags.extend(
[
f"-DCMAKE_CXX_FLAGS='{os.environ['CXXFLAGS']}'",
f"-DCMAKE_C_FLAGS='{os.environ['CFLAGS']}'",
]
)
run(
[
*wasm,
"cmake",
P,
*cmake_flags,
*cmake_args,
f"-DCMAKE_BUILD_TYPE={BUILD_CFG}",
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
f"-DCMAKE_CXX_FLAGS='{os.environ['CXXFLAGS']}'",
f"-DCMAKE_C_FLAGS='{os.environ['CFLAGS']}'",
f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}",
],
cwd=cwd,
@@ -733,7 +627,7 @@ def build_dependency(
if isinstance(patch, str):
patch = [patch]
for p in patch:
patch_abs = (SCRIPT_PATH / p).absolute().__str__()
patch_abs = os.path.abspath(os.path.join(os.path.dirname(__file__), p))
if os.path.exists(patch_abs):
try:
run(["patch", "-p1", "--batch", "--forward", "-i", patch_abs], cwd=extract_dir)
@@ -820,13 +714,12 @@ LDFLAGS = os.environ.get("LDFLAGS", "")
ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if "wasm" in flags:
# WASM `SIDE_MODULE_` are absorbed by `emcmake` automatically.
CXXFLAGS = CXXFLAGS_MINIMAL
CFLAGS = CFLAGS_MINIMAL
CFLAGS_MINIMAL = CXXFLAGS_MINIMAL = CFLAGS = CXXFLAGS = os.environ["SIDE_MODULE_CFLAGS"]
LDFLAGS = os.environ["SIDE_MODULE_LDFLAGS"]
elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if BUILD_STATIC:
CXXFLAGS = f"{CXXFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
CFLAGS = f"{CFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden {ADDITIONAL_ARGS_STR}"
@@ -835,6 +728,8 @@ elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev
CFLAGS = CFLAGS_MINIMAL
LDFLAGS = f"{LDFLAGS} -Wl,--gc-sections {ADDITIONAL_ARGS_STR}"
else:
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if BUILD_STATIC:
CXXFLAGS = f"{CXXFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
CFLAGS = f"{CFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
@@ -914,9 +809,9 @@ if "eigen" in targets:
)
if "pcre" in targets:
OLD_CC, OLD_CXX = None, None
OLD_CC, OLD_CCXX = None, None
if MAC_CROSS_COMPILE_INTEL:
OLD_CC, OLD_CXX = os.environ.get("CC"), os.environ.get("CXX")
OLD_CC, OLD_CCXX = os.environ.get("CC"), os.environ.get("CXX")
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
os.environ["CXX"] = MAC_CROSS_COMPILE_INTEL_CXX
# Keep it autoconf as OpenCOLLADA is pretty old and might break
@@ -929,8 +824,14 @@ if "pcre" in targets:
download_name=f"pcre-{PCRE_VERSION}.tar.bz2",
)
if MAC_CROSS_COMPILE_INTEL:
restore_env("CC", OLD_CC)
restore_env("CXX", OLD_CXX)
if OLD_CC is None:
del os.environ["CC"]
else:
os.environ["CC"] = OLD_CC
if OLD_CCXX is None:
del os.environ["CXX"]
else:
os.environ["CXX"] = OLD_CCXX
if "pcre2" in targets:
build_dependency(
@@ -962,7 +863,7 @@ if "freetype" in targets:
download_url="https://github.com/freetype/freetype",
download_name="freetype2",
download_tool=download_tool_git,
revision="VER-2-14-0",
revision="VER-2-11-1",
)
if USE_OCCT and "occ" in targets:
@@ -979,9 +880,6 @@ if USE_OCCT and "occ" in targets:
if OCCT_VERSION == "7.8.1":
patches.append("./patches/occt/no_ExpToCasExe_7_8_1.patch")
if OCCT_VERSION == "7.9.1":
patches.append("./patches/occt/no_ExpToCasExe_7_9_1.patch")
if "wasm" in flags:
patches.append("./patches/occt/no_em_js.patch")
@@ -1031,20 +929,17 @@ if "libxml2" in targets:
if MAC_CROSS_COMPILE_INTEL:
OLD_CC = os.environ.get("CC")
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
build_tool_args = [
"--without-python",
ENABLE_FLAG,
DISABLE_FLAG,
"--without-zlib",
"--without-iconv",
"--without-lzma",
]
if "wasm" in flags:
build_tool_args.append("--without-threads")
build_dependency(
f"libxml2-{LIBXML2_VERSION}",
"autoconf",
build_tool_args=build_tool_args,
build_tool_args=[
"--without-python",
ENABLE_FLAG,
DISABLE_FLAG,
"--without-zlib",
"--without-iconv",
"--without-lzma",
],
download_url=f"https://download.gnome.org/sources/libxml2/{'.'.join(LIBXML2_VERSION.split('.')[0:2])}/",
download_name=f"libxml2-{LIBXML2_VERSION}.tar.xz",
)
@@ -1101,7 +996,6 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
# On OSX a dynamic python library is built or it would not be compatible
# with the system python because of some threading initialization
PYTHON_CONFIGURE_ARGS: "list[str]" = []
original_path = ""
if platform.system() == "Darwin":
PYTHON_CONFIGURE_ARGS = ["--enable-shared"]
open_ssl_prefix = run([brew, "--prefix", "openssl@3"]).strip()
@@ -1110,10 +1004,6 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
PYTHON_CONFIGURE_ARGS.append(f"--with-openssl={open_ssl_prefix}")
if MAC_CROSS_COMPILE_INTEL:
original_path = os.environ["PATH"]
# Need to ensure python will pick up intel's `pkg-config`,
# otherwise it might attempt to use ARM libraries (e.g. `zstd`) and fail.
os.environ["PATH"] = f"{MAC_INTEL_BIN_PATH}{os.pathsep}{original_path}"
PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"])
for PYTHON_VERSION in PYTHON_VERSIONS:
@@ -1135,9 +1025,6 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
if not os.path.exists(os.path.join(DEPS_DIR, "install", f"python-{PYTHON_VERSION}")):
raise e
if MAC_CROSS_COMPILE_INTEL:
assert original_path
os.environ["PATH"] = original_path
os.environ["CPPFLAGS"] = OLD_CPP_FLAGS
os.environ["CXXFLAGS"] = OLD_CXX_FLAGS
os.environ["CFLAGS"] = OLD_C_FLAGS
@@ -1183,23 +1070,14 @@ if "boost" in targets:
if "cgal" in targets:
gmp_args: "list[str]" = []
mpfr_args: "list[str]" = []
OLD_HOST_CC = None
if WASM:
if APPLE:
# Override `HOST_CC`, otherwise `emcc` will try to use it's own `clang` which can only build
# wasm executables and build will fail.
os.environ["HOST_CC"] = "clang"
# Disable assembly, otherwise `emcc -c conftest.s` will crash due to assembly mismatch.
gmp_args.extend(("--disable-assembly", "--enable-cxx"))
if "wasm" in flags:
gmp_args.extend(("--disable-assembly", "--host", "none", "--enable-cxx"))
mpfr_args.extend(("--host", "none"))
OLD_CC = None
if MAC_CROSS_COMPILE_INTEL:
OLD_CC = os.environ.get("CC")
# Otherwise it's using arm64 `gcc` and fails to build gmp.
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
build_dependency(
name=f"gmp-{GMP_VERSION}",
@@ -1208,14 +1086,10 @@ if "cgal" in targets:
pre_compile_subs=(
[("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []
),
# Sometimes ftp.gnu.org is very slow, use ftpmirror.gnu.org as a workaround.
download_url="https://ftpmirror.gnu.org/gnu/gmp/",
download_url="https://ftp.gnu.org/gnu/gmp/",
download_name=f"gmp-{GMP_VERSION}.tar.bz2",
)
if WASM and APPLE:
restore_env("HOST_CC", OLD_HOST_CC)
build_dependency(
name=f"mpfr-{MPFR_VERSION}",
mode="autoconf",
@@ -1225,7 +1099,10 @@ if "cgal" in targets:
)
if MAC_CROSS_COMPILE_INTEL:
restore_env("CC", OLD_CC)
if OLD_CC is None:
del os.environ["CC"]
else:
os.environ["CC"] = OLD_CC
build_dependency(
name=f"cgal-{CGAL_VERSION}",
@@ -1356,13 +1233,7 @@ def get_cmake_args_prefix_path(additional_paths: "Sequence[str]" = ()) -> "list[
args_prefix_path = cmake_args_prefix_path.copy()
args_prefix_path.extend(additional_paths)
prefix_path = ";".join(args_prefix_path)
if WASM:
# `emcmake` is disabling search in PATH, so we provide root paths instead.
# Provide '/' to PATH, so it will be combined with provided root paths,
# otherwise, depending on environment, it might not search the root path itself.
return [f"-DCMAKE_FIND_ROOT_PATH={prefix_path}", "-DCMAKE_PREFIX_PATH=//"]
else:
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
if "wasm" in flags:
@@ -1430,7 +1301,7 @@ if "rocksdb" in targets:
]
)
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets):
logger.info("\rConfiguring executables...")
exec_args = [
@@ -1452,11 +1323,11 @@ if "IfcOpenShell-Python" in targets:
# On OSX the actual Python library is not linked against.
ADDITIONAL_ARGS = ""
if platform.system() == "Darwin":
ADDITIONAL_ARGS = "-Wl,-undefined,dynamic_lookup"
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
if "wasm" in flags:
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
# So cache doesn't match and running build-all.py builds most of ifcopenshell libraries twice.
os.environ["CPPFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
@@ -1466,47 +1337,40 @@ if "IfcOpenShell-Python" in targets:
os.makedirs(python_dir, exist_ok=True)
def compile_python_wrapper(
python_version: str,
python_include: Union[str, None] = None,
python_executable: Union[str, None] = None,
python_path: Union[Path, None] = None,
python_version: str, python_library: str, python_include: str, python_executable: Union[str, None]
) -> Union[str, None]:
"""
:return: Path to module dir if ``python_executable`` was provided, otherwise ``None``.
"""
assert bool(python_path) ^ bool(python_include)
logger.info(f"\rConfiguring python {python_version} wrapper...")
cache_path = os.path.join(python_dir, "CMakeCache.txt")
if os.path.exists(cache_path):
os.remove(cache_path)
prefix_paths: list[str] = []
if "swig" in targets:
prefix_paths.append(f"{DEPS_DIR}/install/swig")
if python_path:
# We couldn't just prefix PATH and have to provide all variables explicitly,
# see ifcwrap/cmake for the details.
python_executable = (Path(python_path) / "bin" / "python3").__str__()
python_include = run(
[
python_executable,
"-c",
"import sysconfig; print(sysconfig.get_config_var('INCLUDEPY'))",
]
)
os.environ["PYTHON_LIBRARY_BASENAME"] = os.path.basename(python_library)
swig_prefix_paths: list[str] = []
if "swig" in targets:
swig_prefix_paths.append(f"{DEPS_DIR}/install/swig")
assert python_include
run_cmake(
"",
cmake_args
+ get_cmake_args_prefix_path(prefix_paths)
+ get_cmake_args_prefix_path(swig_prefix_paths)
+ [
"-DPYTHON_LIBRARY=" + python_library,
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
# *([f"-DPYTHON_MODULE_INSTALL_DIR={os.environ['PYTHONPATH']}/ifcopenshell"] if "wasm" in flags else []),
*(
[
"-DPYTHON_MODULE_INSTALL_DIR="
+ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "package"))
]
if "wasm" in flags
else []
),
"-DPYTHON_INCLUDE_DIR=" + python_include,
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
@@ -1517,7 +1381,7 @@ if "IfcOpenShell-Python" in targets:
logger.info(f"\rBuilding python {python_version} wrapper... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper"], cwd=python_dir)
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
if python_executable:
@@ -1544,19 +1408,32 @@ if "IfcOpenShell-Python" in targets:
if "wasm" in flags:
compile_python_wrapper(
run(["pyodide", "config", "get", "python_version"]),
run(["pyodide", "config", "get", "python_include_dir"]),
f"{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.{os.environ['PYMICRO']}",
f"{os.environ['TARGETINSTALLDIR']}/lib/libpython{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.a",
os.environ["PYTHONINCLUDE"],
None,
)
# Copy setup.py where pyodide build system expects it.
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
elif USE_CURRENT_PYTHON_VERSION:
python_info = sysconfig.get_paths()
compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable)
py_path_components = [sysconfig.get_config_var("LIBDIR"), sysconfig.get_config_var("INSTSONAME")]
if sysconfig.get_config_var("multiarchsubdir"):
py_path_components.insert(1, sysconfig.get_config_var("multiarchsubdir").replace("/", ""))
python_lib = os.path.join(*py_path_components)
compile_python_wrapper(platform.python_version(), python_lib, python_info["include"], sys.executable)
else:
for python_version in PYTHON_VERSIONS:
python_path = Path(DEPS_DIR) / "install" / f"python-{python_version}"
module_dir = compile_python_wrapper(python_version, python_path=python_path)
python_library = run([bash, "-c", f"ls {DEPS_DIR}/install/python-{python_version}/lib/libpython*.*"])
python_include = run([bash, "-c", f"ls -d {DEPS_DIR}/install/python-{python_version}/include/python*"])
python_executable = os.path.join(
DEPS_DIR, "install", f"python-{python_version}", "bin", f"python{python_version[0]}"
)
module_dir = compile_python_wrapper(python_version, python_library, python_include, python_executable)
assert module_dir
# Not sure why, but added after reading this in the logs
# cp: /Users/runner/work/IfcOpenShell/IfcOpenShell/build/Darwin/x86_64/10.15/install/ifcopenshell/python-3.9.11: No such file or directory
@@ -1,13 +0,0 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 34300d41ad..09b2e0d45f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -721,6 +721,8 @@ else()
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
endif()
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
+
# bison
if (BUILD_YACCLEX)
list (APPEND OCCT_3RDPARTY_CMAKE_LIST "adm/cmake/bison")
-32
View File
@@ -1,32 +0,0 @@
There are two ways to build pyodide ifcopenshell Python wrapper wheel.
1. Using pyodide build system (`build_pyodide.yml` does it):
- install prebuilt pyodide build and emscripten environment (see `build_pyodide.sh`)
- clone IfcOpenShell to `IfcOpenShell` folder
- create `packages/ifcopenshell` folder that will be used by pyodide build system
- from `IfcOpenShell` move building recipe `pyodide/meta.yaml` to `packages/ifcopenshell`
- run `pyodide build-recipes ifcopenshell --install`, it will
- execute `meta.yaml` recipe - it will:
- copy IfcOpenShell source to build folder `packages/ifcopenhell/build/ifcopenshell-0.8.0`
- build ifcopenshell and its dependencies
- note that rerunning `pyodide build-recipes` will remove previous build folder and rebuild all dependencies.
The way to avoid it, if build fails, is to use `pyodide build-recipes-no-deps ifcopenshell --continue` instead.
- run `setup.py` in `IfcOpenShell` root, producing a wheel in `IfcOpenShell/dist`
- copy that wheel to `packages/ifcopenshell/dist`
- `--install` it to current build envrionment
- copy the wheel next to `dist` folder (in root directory, next to `packages`)
- add wheel to `dist/pyodide-lock.json`
2. Build it outside of pyodide build system.
Building inside pyodide build system should be preferred, option to build it outside is useful for debugging purposes,
since it's pure cmake without any additional moving parts.
- setup pyodide environment, see above
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
- it will produce Python package in `IfcOpenShell/ifcopenshell`
- run `pyodide build`
- it will produce a wheel in `IfcOpenShell/dist`
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/bash
set -ex
# Install uv.
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv --python 3.13
source .venv/bin/activate
# Install pyodide cross build environment.
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
uv pip install pyodide-build
# `uv run` is required, so xbuildenv would skip using `pip`.
uv run pyodide xbuildenv install
# Emscripten doesn't come with xbuildenv.
git clone https://github.com/emscripten-core/emsdk
pushd emsdk
PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)
./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}
./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}
source emsdk_env.sh
which emcc
popd
mkdir -p packages/ifcopenshell
VERSION=`cat IfcOpenShell/VERSION`
cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell
sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
# Use custom build ifcopenshell directory in build-all to make caching simpler
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
export BUILD_DIR=`readlink -f ifcopenshell_build`
# Use build-recipes-no-deps first, so logs would be printed to stdout.
pyodide build-recipes-no-deps ifcopenshell
pyodide build-recipes ifcopenshell --install
-64
View File
@@ -1,64 +0,0 @@
"""
Cache built dependencies for builds.
This script is finding common install directory and either
packs each folder into a tar.gz archive, if it wasn't packed before,
or unpacks existing archives.
Usage: python cache_dependencies.py [pack|unpack]
"""
import tarfile
import sys
from pathlib import Path
from typing import Literal
CACHE_PREFIX = "cache-"
def get_install_dir() -> Path:
for data in Path.cwd().glob("*/*/install"):
return data
raise Exception("No install dir found")
def pack_dependencies(install_dir: Path) -> None:
# Process each install_dir
for dependency_path in install_dir.iterdir():
if not dependency_path.is_dir():
continue
dependency_name = dependency_path.name
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
if tar_path.exists():
print(f"Skipping existing cache: '{tar_path}'")
else:
with tarfile.open(tar_path, "w:gz") as tar:
tar.add(dependency_path, arcname=dependency_path.name)
print(f"Created cache: '{tar_path}'")
def unpack_dependencies(install_dir: Path) -> None:
# `filter` argument was fully introduced in 3.12
# and results in deprecation warnings in 3.12-3.13, if not provided.
tar_filter: dict[Literal["filter"], Literal["data"]] = (
{"filter": "data"} if bool(sys.version_info >= (3, 12)) else {}
)
for tar_path in install_dir.glob(f"{CACHE_PREFIX}*.tar.gz"):
with tarfile.open(tar_path, "r:gz") as tar:
tar.extractall(path=install_dir, **tar_filter)
print(f"Extracted cache: '{tar_path.name}'.")
if __name__ == "__main__":
if len(sys.argv) != 2 or (action := sys.argv[1].lower()) not in ("pack", "unpack"):
print(__doc__)
sys.exit(1)
install_dir = get_install_dir()
print(f"Found install dir: '{install_dir}'")
if action == "pack":
pack_dependencies(install_dir)
else:
unpack_dependencies(install_dir)
+3 -2
View File
@@ -3,12 +3,13 @@ package:
version: 0.8.0
source:
# meta.yaml is placed as `packages/ifcopenshell/meta.yaml`.
path: ../../IfcOpenShell
build:
script: |
BUILD_CFG=Release python nix/build-all.py -v --wasm --py313
BUILD_CFG=Release python nix/build-all.py --without-rocksdb --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py313 IfcOpenShell-Python
mv package/ifcopenshell .
cp pyodide/setup.py .
about:
home: http://ifcopenshell.org
+9 -45
View File
@@ -1,47 +1,11 @@
# setup.py is getting deprecated, but we still use it,
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
# and we need it to get the wheel suffix right.
import os
from pathlib import Path
from setuptools import setup, find_packages
import tomllib
from setuptools import Extension, find_packages, setup
REPO_FOLDER = Path(__file__).parent
def get_version() -> str:
if "PKG_VERSION" in os.environ:
# Inside pyodide build environment.
return os.environ["PKG_VERSION"]
return (REPO_FOLDER / "VERSION").read_text().strip()
# Read dependencies from pyproject.toml
def get_dependencies() -> list[str]:
pyproject_toml = REPO_FOLDER / "src" / "ifcopenshell-python" / "pyproject.toml"
pyproject_data = tomllib.loads(pyproject_toml.read_text())
dependencies = pyproject_data["project"]["dependencies"]
return dependencies
setup(
name="ifcopenshell",
version=get_version(),
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="https://ifcopenshell.org",
install_requires=get_dependencies(),
packages=find_packages(include=["ifcopenshell", "ifcopenshell.*"]),
package_data={
# "*.so" is needed to include prebuilt binary extension. Otherwise it would try to build it and fail.
"ifcopenshell": ["util/schema/*.json", "util/schema/*.ifc", "*.so"],
"": ["*.json", "*.ifc"],
},
# Has to provide extension to get the correct wheel suffix.
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
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']},
)
-27
View File
@@ -1,27 +0,0 @@
from pathlib import Path
WHEEL_FILENAME = next(
p.name for p in (Path.cwd() / "pyodide").iterdir() if p.name.startswith("ifcopenshell-") and p.suffix == ".whl"
)
def test_ifcopenshell_import(selenium):
selenium.load_package("micropip")
# Important to test it with `micropip.install`
# without any dependencies loaded to ensure micropip will load them automatically.
selenium.run_async(
f"""
import micropip
await micropip.install(f"./{WHEEL_FILENAME}")
import ifcopenshell
ifc_file = ifcopenshell.file()
wall = ifc_file.create_entity("IfcWall")
wall1 = ifc_file.by_type("IfcWall")[0]
print(wall, wall1)
assert wall == wall1, "Wall entity doesn't match"
wall.Name = "Test"
assert wall.Name == "Test", f"Entity name wasn't changed: {{wall}}"
print(wall)
"""
)
+3 -12
View File
@@ -38,7 +38,6 @@ exclude = [
"src/ifc2ca/templates",
]
[tool.ruff.lint]
preview = true
select = [
# Default Ruff rules.
# "E4", # imports
@@ -61,15 +60,7 @@ ignore = [
"UP030", # implicit references for positional format fields
"UP031", # Replace % with .format
"UP032", # Replace .format with f-string
# Deprecated Ruff rules.
"UP038", # Use X | Y in isinstance
]
[tool.poe.tasks]
ruff-main = "ruff check --extend-exclude nix/build-all.py"
# It's actually Python 3.6, but ruff only supports 3.7+, but it should do.
ruff-old = "ruff check nix/build-all.py --target-version py37"
ruff.sequence = ["ruff-main", "ruff-old"]
black = "black ."
format.sequence = ["black", "ruff-main", "ruff-old"]
+12 -3
View File
@@ -6,7 +6,7 @@ from numpy.typing import NDArray
def camera_vectors_from_element_placement(
elem_placement: NDArray[np.float64],
) -> tuple[list[float], list[float], list[float]]:
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
"""
Calculate the vectors of a camera pointing to an element.
@@ -22,7 +22,7 @@ def camera_vectors_from_element_placement(
def camera_vectors_from_target_position(
target_position: NDArray[np.float64], offset: Optional[NDArray[np.float64]] = None
) -> tuple[list[float], list[float], list[float]]:
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
"""
Calculate the vectors of a camera pointing to a target point.
@@ -38,7 +38,16 @@ def camera_vectors_from_target_position(
camera_direction = unit_vector(-camera_offset) # pylint: disable=invalid-unary-operand-type
camera_right = unit_vector(np.cross(np.array([0.0, 0.0, 1.0]), camera_direction))
camera_up = unit_vector(np.cross(camera_direction, camera_right))
return camera_position.tolist(), camera_direction.tolist(), camera_up.tolist()
return camera_position, camera_direction, camera_up
# rotation_transform = np.eye(4)
# rotation_transform[0, :3] = camera_right
# rotation_transform[1, :3] = camera_up
# rotation_transform[2, :3] = camera_direction
# translation_transform = np.eye(4)
# translation_transform[:3, -1] = -camera_position
# look_at_transform = np.matmul(rotation_transform, translation_transform)
# mat = np.linalg.inv(look_at_transform)
# return camera_position, -mat[:3, 2], mat[:3, 1]
def unit_vector(v: NDArray[np.float64]) -> NDArray[np.float64]:
-2
View File
@@ -192,8 +192,6 @@ class TopicHandler:
bcf_zip: The BCF zip file to save to.
"""
topic_dir = self.guid
# simulating directory creation (ZipFile in python < 3.11 doesn't have mkdir)
destination_zip.writestr(f"{topic_dir}/", "")
self._save_xml(destination_zip, self._markup, "markup.bcf")
self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip)
+4 -2
View File
@@ -319,7 +319,9 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
ifc_file = element.wrapped_data.file
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
elem_placement[:3, 3] *= unit_scale
elem_placement[0][3] *= unit_scale
elem_placement[1][3] *= unit_scale
elem_placement[2][3] *= unit_scale
return mdl.VisualizationInfo(
guid=str(uuid.uuid4()),
@@ -379,7 +381,7 @@ def build_camera(elem_placement: NDArray[np.float64]) -> mdl.PerspectiveCamera:
def build_camera_from_vectors(
camera_position: list[float], camera_dir: list[float], camera_up: list[float]
camera_position: NDArray[np.float64], camera_dir: NDArray[np.float64], camera_up: NDArray[np.float64]
) -> mdl.PerspectiveCamera:
"""
Return a BCF camera for an IFC element placement matrix.
-2
View File
@@ -179,8 +179,6 @@ class TopicHandler:
bcf_zip: The BCF zip file to save to.
"""
topic_dir = self.guid
# simulating directory creation (ZipFile in python < 3.11 doesn't have mkdir)
destination_zip.writestr(f"{topic_dir}/", "")
self._save_xml(destination_zip, self._markup, "markup.bcf")
self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip)
+4 -2
View File
@@ -319,7 +319,9 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
ifc_file = element.wrapped_data.file
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
elem_placement[:3, 3] *= unit_scale
elem_placement[0][3] *= unit_scale
elem_placement[1][3] *= unit_scale
elem_placement[2][3] *= unit_scale
return mdl.VisualizationInfo(
guid=str(uuid.uuid4()),
@@ -379,7 +381,7 @@ def build_camera(elem_placement: NDArray[np.float64]) -> mdl.PerspectiveCamera:
def build_camera_from_vectors(
camera_position: list[float], camera_dir: list[float], camera_up: list[float]
camera_position: NDArray[np.float64], camera_dir: NDArray[np.float64], camera_up: NDArray[np.float64]
) -> mdl.PerspectiveCamera:
"""
Return a BCF camera for an IFC element placement matrix.
+2 -2
View File
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=6924012
OLD:=e795cec
.PHONY: bump
bump:
ifndef NEW
@@ -127,7 +127,7 @@ endif
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download GitPython --dest=./wheels
# Provides audio playback for costing
# This is a REALLY IMPORTANT feature
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel git+https://github.com/zdhoward/aud --wheel-dir=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel git+https://github.com/Andrej730/aud.git --wheel-dir=./wheels
# IfcOpenShell dependency - support for new typing features
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download typing_extensions --dest=./wheels
# Required by IfcCSV
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 B

@@ -13,10 +13,10 @@ DATA;
#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','Whether or not this drawing can be referenced in other drawings.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
@@ -5,10 +5,10 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Anno
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4,#29));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separated 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.);
#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.);
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/BATTING,IfcTypeProduct',(#6,#7));
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
@@ -36,7 +36,5 @@ DATA;
#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')),$);
#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
+1 -11
View File
@@ -36,7 +36,6 @@ from bonsai.bim.ifc import IfcStore
from mathutils import Vector
from typing import Union
from logging import Logger
from math import radians
class IfcExporter:
@@ -105,16 +104,7 @@ class IfcExporter:
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)
# Handle camera scales specially
if obj.type == "CAMERA":
# Check if this is a reflected ceiling plan camera
camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
# Ensure reflected ceiling cameras have the correct scale
if obj.scale != (-1, -1, -1):
obj.scale = (-1, -1, -1)
# Skip all other scale handling for cameras
elif tool.Geometry.is_scaled(obj):
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
# reset it, so let user know that the scale wasn't saved.
+2
View File
@@ -222,6 +222,8 @@ def refresh_ui_data():
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
ifc_file.clear_cache()
props = tool.Drawing.get_document_props()
props.should_draw_decorations = props.should_draw_decorations
if tool.Web.get_web_props().is_connected:
tool.Web.send_webui_data()
-1
View File
@@ -556,7 +556,6 @@ def draw_filter(
elif ifc_filter.type == "query":
row = box.row(align=True)
row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA")
row.prop(ifc_filter, "comparison", text="")
row.prop(ifc_filter, "value", text="")
elif ifc_filter.type == "instance":
row = box.row(align=True)
+11 -37
View File
@@ -283,7 +283,6 @@ class IfcImporter:
self.place_objects_in_collections()
self.profile_code("Place objects in collections")
self.setup_arrays()
self.update_linked_aggregates()
self.profile_code("Setup arrays")
tool.Project.load_linked_models_from_ifc()
self.profile_code("Load linked models")
@@ -625,13 +624,18 @@ class IfcImporter:
print("Done creating geometry")
def create_spatial_elements(self) -> None:
self.create_generic_elements(self.spatial_elements)
if tool.Blender.get_addon_preferences().spatial_elements_unselectable:
self.create_generic_elements(self.spatial_elements, unselectable=True)
else:
self.create_generic_elements(self.spatial_elements, unselectable=False)
def create_elements(self) -> None:
self.create_generic_elements(self.elements)
self.create_generic_elements(self.gross_elements, is_gross=True)
def create_generic_elements(self, elements: set[ifcopenshell.entity_instance], is_gross=False) -> None:
def create_generic_elements(
self, elements: set[ifcopenshell.entity_instance], unselectable=False, is_gross=False
) -> None:
if isinstance(self.file, ifcopenshell.sqlite):
return self.create_generic_sqlite_elements(elements)
@@ -655,6 +659,10 @@ class IfcImporter:
print("{} / {} elements processed ...".format(i, total))
objects.add(self.create_product(element))
if unselectable:
for obj in objects:
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])
@@ -952,9 +960,6 @@ class IfcImporter:
if not props.ifc_file:
props.ifc_file = self.ifc_import_settings.input_file
self.file = tool.Ifc.get()
# IFC4 Reference View shall have no booleans https://github.com/BuildingSMART/IFC4-CV/issues/14
if self.file.schema == "IFC4" and "ReferenceView" in str(self.file.header.file_description.description):
self.ifc_import_settings.void_limit = 0
def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -1012,7 +1017,6 @@ class IfcImporter:
)
obj = self.create_product(self.project["ifc"])
obj.hide_select = True
obj.hide_viewport = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
props = tool.Blender.get_object_bim_props(obj)
@@ -1214,36 +1218,6 @@ class IfcImporter:
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
def update_linked_aggregates(self):
# TODO Remove this after a while. See commit 17d6b8a
# https://github.com/IfcOpenShell/IfcOpenShell/commit/17d6b8af61c8dd7cd82f6e72b2fb3831d851a33d#commitcomment-163151609
groups = self.file.by_type("IfcGroup")
target_groups = [group for group in groups if group.Name == "BBIM_Linked_Aggregate"]
if not target_groups:
return
for i, group in enumerate(target_groups):
name = "Default"
elements = ifcopenshell.util.element.get_grouped_by(self.file.by_id(group.id()), is_recursive=True)
for j, element in enumerate(elements):
split = element.Name.rsplit("_")
name = split[0]
try:
aggregate_index = int(split[1])
except:
aggregate_index = 0
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate", should_inherit=False)
if not pset:
return
if "Aggregate_Index" not in pset.keys():
ifcopenshell.api.run(
"pset.edit_pset",
self.file,
pset=self.file.by_id(pset["id"]),
properties={"Aggregate_Index": aggregate_index, "Name": name},
)
class IfcImportSettings:
"""
@@ -94,9 +94,6 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
aggregates_to_check = set()
# First pass: unassign all parts and track their aggregates
for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj)
if not element:
@@ -104,10 +101,6 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
continue
# Track this aggregate for later checking
aggregates_to_check.add(aggregate)
core.unassign_object(
tool.Ifc,
tool.Aggregate,
@@ -123,29 +116,6 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
# Second pass: delete aggregates that now have no parts
deleted_aggregates = []
for aggregate in aggregates_to_check:
related_objects = ifcopenshell.util.element.get_parts(aggregate)
if len(related_objects) == 0:
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
deleted_aggregates.append(aggregate_name)
aggregate_obj = tool.Ifc.get_object(aggregate)
if aggregate_obj:
ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate)
bpy.data.objects.remove(aggregate_obj, do_unlink=True)
# Show info message if aggregates were deleted
if deleted_aggregates:
if len(deleted_aggregates) == 1:
self.report(
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
)
else:
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
"""Enable editing aggregation relationship"""
+2 -2
View File
@@ -1215,7 +1215,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
assert blender_topic
topic = bcfxml.topics[blender_topic.name]
if self.viewpoint_guid:
viewpoint_guid = self.viewpoint_guid + ".bcfv"
viewpoint_guid = self.viewpoint_guid
if viewpoint_guid not in topic.viewpoints:
self.report({"ERROR"}, f"No such viewpoint in the active topic: '{viewpoint_guid}'.")
return {"CANCELLED"}
@@ -1313,7 +1313,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
x_axis = y_axis.cross(z_axis).normalized()
rotation = Matrix((x_axis, y_axis, z_axis))
rotation.invert()
matrix = np.array(
matrix = np.matrix(
(
[x_axis[0], y_axis[0], z_axis[0], camera.camera_view_point.x],
[x_axis[1], y_axis[1], z_axis[1], camera.camera_view_point.y],
@@ -282,11 +282,7 @@ class BIM_PT_classification_references(Panel, ReferenceUI):
@classmethod
def poll(cls, context):
return (
(obj := tool.Blender.get_active_object())
and (element := tool.Ifc.get_entity(obj))
and element.is_a("IfcObjectDefinition")
)
return bool((obj := context.active_object) and tool.Ifc.get_entity(obj))
def get_object_name(self, context: bpy.types.Context) -> str:
assert (obj := context.active_object)
+3 -27
View File
@@ -818,12 +818,7 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_identical_objects"
bl_label = "Merge Identical Objects"
bl_description = (
"Merge identical IFC objects (that match all attributes).\n"
"\n"
"SHIFT + CLICK to merge by name/identification attribute only.\n"
"Merges names with number suffix, as well (ex: foo, foo.001, foo.002)\n"
)
bl_description = "For materials currently only IfcMaterials are supported"
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
@@ -831,36 +826,18 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
)
by_name_or_identification_only: bpy.props.BoolProperty(
name="By Name/Identification Only",
description="Merge based only on Name or Identification attribute, ignoring other properties",
default=False,
)
if TYPE_CHECKING:
object_type: tool.Debug.PurgeMergeObjectType
def invoke(self, context, event):
# Check if shift key is pressed
if event.shift:
self.by_name_or_identification_only = True
else:
self.by_name_or_identification_only = False
return self.execute(context)
def _execute(self, context):
object_type: str = self.object_type
if object_type in ("PROFILE", "TYPE"):
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
return {"CANCELLED"}
merged_data = tool.Debug.merge_identical_objects(
object_type, by_name_or_identification_only=self.by_name_or_identification_only
)
merged_data = tool.Debug.merge_identical_objects(object_type)
plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
if merged_data:
merge_mode = " by name/identification" if self.by_name_or_identification_only else ""
for element_type, element_names in merged_data.items():
print(f"- {element_type}:")
for name in element_names:
@@ -869,8 +846,7 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
merged = sum(len(v) for v in merged_data.values())
msg = " See system console for details." if merged else ""
merge_mode = " (by name/identification)" if self.by_name_or_identification_only else ""
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged{merge_mode}.{msg}")
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}")
if merged == 0:
return
@@ -61,7 +61,6 @@ classes = (
operator.EnableEditingAssignedProduct,
operator.EnableEditingElementFilter,
operator.EnableEditingText,
operator.ExcludeAnnotation,
operator.ExpandSheet,
operator.LoadDrawings,
operator.LoadReferences,
+8 -21
View File
@@ -235,18 +235,12 @@ class DecoratorData:
cut_cache = {}
slice_cache = {}
fill_cache = {}
camera_location_checksum = None
camera_rotation_checksum = None
@classmethod
def clear_cache(cls):
cls.cut_cache = {}
cls.layerset_cache = {}
cls.fill_cache = {}
@classmethod
def load(cls, handler):
cls.is_loaded = True
cls.cut_cache = {}
cls.layerset_cache = {}
text = {}
dimension = {}
@@ -352,10 +346,12 @@ class DecoratorData:
(font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
)
font_size = FONT_SIZES[font_size_type]
# get symbol
symbol = tool.Drawing.get_annotation_symbol(element)
# get newline_at
newline_at = pset_data.get("Newline_At", 0)
reverse_list = pset_data.get("Reverse_List", False)
list_separator = pset_data.get("List_Separator") or ", "
# other attributes
literals = tool.Drawing.get_text_literal(obj, return_list=True)
@@ -367,20 +363,11 @@ class DecoratorData:
literal_data = {
"Literal": literal_value,
"BoxAlignment": literal.BoxAlignment,
"CurrentValue": tool.Drawing.replace_text_literal_variables(
literal_value, product, reverse_list, list_separator
),
"CurrentValue": tool.Drawing.replace_text_literal_variables(literal_value, product),
}
literals_data.append(literal_data)
return {
"Literals": literals_data,
"FontSize": font_size,
"Symbol": symbol,
"Newline_At": newline_at,
"Reverse_List": reverse_list,
"List_Separator": list_separator,
}
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
@classmethod
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
@@ -22,7 +22,6 @@ import blf
import math
import bmesh
import shapely
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
@@ -1626,7 +1625,6 @@ class CutDecorator:
if cls.installed:
cls.uninstall()
handler = cls()
handler.cache_camera_matrix()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
@@ -1729,30 +1727,6 @@ class CutDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def cache_camera_matrix(self):
obj = bpy.context.scene.camera
DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
def is_camera_moved(self):
if not DecoratorData.camera_location_checksum:
self.cache_camera_matrix()
return True # Let's be conservative
obj = bpy.context.scene.camera
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
loc_real = np.array(obj.matrix_world.translation).flatten()
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
self.cache_camera_matrix()
return True
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3())
rot_dot = np.dot(rot_check, rot_real.T)
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
if angle_rad > 0.0017453292519943296: # 0.1 degrees
self.cache_camera_matrix()
return True
return False
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
has_cut_cache = element.id() in DecoratorData.cut_cache
has_fill_cache = element.id() in DecoratorData.fill_cache
@@ -1760,9 +1734,9 @@ class CutDecorator:
# Currently selected objects must be recalculated as they may be being moved / edited.
# If the camera is selected, we also recalculate as the user may be moving the camera.
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_cut(context, obj, element)
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
@@ -1959,7 +1933,8 @@ class DecorationsHandler:
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
DecoratorData.clear_cache()
if not DecoratorData.is_loaded:
DecoratorData.load(handler)
@classmethod
def uninstall(cls):
@@ -1986,8 +1961,5 @@ class DecorationsHandler:
if not DrawingsData.is_loaded:
DrawingsData.load()
if not DecoratorData.is_loaded:
DecoratorData.load(self)
for obj, decorator in DecoratorData.data["object_decorators"]:
decorator.decorate(context, obj)
+75 -112
View File
@@ -106,17 +106,14 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
obj = bpy.data.objects.new(object_type, None)
obj.name = props.type_name
ifc_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW")
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcTypeProduct",
predefined_type=object_type,
should_add_representation=has_representation,
context=ifc_context,
context=ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW"),
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
if representation := tool.Drawing.get_representation(element, ifc_context):
tool.Drawing.reload_representation(obj=obj, representation=representation)
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
if props.create_representation_for_type and object_type == "IMAGE":
@@ -210,6 +207,12 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
should_duplicate_annotations=self.should_duplicate_annotations,
)
# TODO: Why need to resync active drawing, if it wasn't changed.
drawing = props.get_active_drawing()
if drawing is None:
return
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
class CreateDrawing(bpy.types.Operator):
"""Creates/refreshes a .svg drawing
@@ -432,19 +435,21 @@ class CreateDrawing(bpy.types.Operator):
if os.path.isfile(svg_path) and self.props.should_use_underlay_cache:
return svg_path
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
for obj in bpy.context.view_layer.objects:
obj.hide_render = obj.name not in visible_object_names
assert context.scene and context.view_layer and context.screen
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
assert (drawing_style := self.cprops.get_active_drawing_style())
tool.Blender.sync_render_visibility()
if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True)
else:
previous_visibility: dict[str, bool] = {}
collection = tool.Blender.get_object_bim_props(self.camera).collection
assert collection
# Hide annotations.
# Hie annotations.
for obj in collection.objects:
if context.view_layer.objects.get(obj.name):
previous_visibility[obj.name] = obj.hide_get()
@@ -621,7 +626,7 @@ class CreateDrawing(bpy.types.Operator):
path.attrib["d"] = d
group.append(g)
def generate_material_layers(self, context: bpy.types.Context, root) -> None:
def generate_wall_layers(self, context: bpy.types.Context, root) -> None:
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" in el.get("class", "").split():
continue
@@ -790,9 +795,8 @@ class CreateDrawing(bpy.types.Operator):
edge_bm.to_mesh(edge_mesh)
edge_bm.free()
freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter")
actual_path = svg_path[0:-4] + "0001.svg"
context.scene.render.filepath = svg_path[0:-4]
actual_path = freestyle_svg_exporter.create_path(bpy.context.scene)
bpy.ops.render.render(write_still=False)
os.replace(actual_path, svg_path)
@@ -836,8 +840,6 @@ class CreateDrawing(bpy.types.Operator):
if tool.Drawing.is_camera_orthographic():
self.generate_bisect_linework(context, root)
if self.cprops.generate_material_layers:
self.generate_material_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -935,14 +937,12 @@ class CreateDrawing(bpy.types.Operator):
if self.cprops.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
if self.cprops.generate_material_layers:
self.generate_material_layers(context, root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
elif self.cprops.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
if self.cprops.generate_material_layers:
self.generate_material_layers(context, root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -1252,8 +1252,6 @@ class CreateDrawing(bpy.types.Operator):
def get_svg_classes(self, element, layer=None):
classes = [element.is_a()]
# ─── Material ──────────────────────────────────────────────
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
material_name = ""
if material:
@@ -1266,32 +1264,18 @@ class CreateDrawing(bpy.types.Operator):
else:
classes.append("material-null")
# ─── Layer ─────────────────────────────────────────────────
if layer:
classes.append(layer.is_a())
layer_material = layer.Material
layer_material_name = layer_material.Name or "null"
layer_material_name = tool.Drawing.canonicalise_class_name(layer_material_name)
classes.append(f"layer-material-{layer_material_name}")
material_name = layer.Material.Name or "null"
material_name = tool.Drawing.canonicalise_class_name(material_name)
classes.append(f"layer-material-{material_name}")
# Add material category if available
if hasattr(layer_material, "Category") and layer_material.Category:
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
classes.append(f"layer-material-category-{layer_material_category}")
# ─── Metadata ──────────────────────────────────────────────
for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key)
if value:
classes.append(
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
)
# ─── Target View ───────────────────────────────────────────
if getattr(self.cprops, "target_view", None):
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
classes.append(f"target-view-{target_view_class}")
return classes
def is_manifold(self, obj) -> bool:
@@ -1358,13 +1342,7 @@ class CreateDrawing(bpy.types.Operator):
join_criteria = join_criteria.split(",")
else:
# Drawing convention states that same objects classes with the same material are merged when cut.
join_criteria = [
"class",
"material.Name",
"/Pset_.*Common/.Status",
"EPset_Status.Status",
"EPset_Status.UserDefinedStatus",
]
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
group = root.find("{http://www.w3.org/2000/svg}g")
joined_paths = {}
@@ -1493,10 +1471,11 @@ class CreateDrawing(bpy.types.Operator):
joined_paths.setdefault(hash_keys, []).append(el)
for key, els in joined_paths.items():
queue = []
polygons = []
classes = set()
for el in els:
classes = set(el.attrib["class"].split())
classes.update(el.attrib["class"].split())
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
is_closed_polygon = False
for path in el.findall("{http://www.w3.org/2000/svg}path"):
@@ -1511,30 +1490,31 @@ class CreateDrawing(bpy.types.Operator):
coords.append(coords[0])
if len(coords) > 2 and coords[0] == coords[-1]:
is_closed_polygon = True
queue.append((shapely.Polygon(coords), classes))
polygons.append(shapely.Polygon(coords))
if is_closed_polygon:
el.getparent().remove(el)
while queue:
polygon, polygon_classes = queue.pop()
for polygon2, polygon2_classes in queue[:]:
try:
merged_polygon = shapely.union(polygon, polygon2)
except:
print("Warning. Portions of the merge failed. Please report a bug!", polygon, polygon2)
continue
if type(merged_polygon) == shapely.Polygon:
polygon = merged_polygon
polygon_classes.update(polygon2_classes)
queue.remove((polygon2, polygon2_classes))
try:
merged_polygons = shapely.ops.unary_union(polygons)
except:
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
merged_polygons = polygons
if type(merged_polygons) == shapely.MultiPolygon:
merged_polygons = merged_polygons.geoms
elif type(merged_polygons) == shapely.Polygon:
merged_polygons = [merged_polygons]
else:
merged_polygons = []
for polygon in merged_polygons:
g = etree.Element("g")
path = etree.SubElement(g, "path")
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
for interior in polygon.interiors:
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
path.attrib["d"] = d
g.set("class", " ".join(list(polygon_classes)))
g.set("class", " ".join(list(classes)))
group.append(g)
def drawing_to_model_co(self, x: float, y: float) -> Vector:
@@ -1876,15 +1856,13 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = tool.Drawing.get_document_props()
# Won't be visible in UI anyway.
prefs = tool.Blender.get_addon_preferences()
if not props.sheets or not prefs.data_dir:
return False
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
if not props.sheets:
cls.poll_message_set("No sheets available.")
return False
if not tool.Blender.get_user_data_dir():
cls.poll_message_set("BIM data directory not set.")
return False
return True
def _execute(self, context):
@@ -2187,10 +2165,27 @@ class ActivateModel(bpy.types.Operator):
if model_props.show_cut_decorator:
CutDecorator.uninstall()
# Preserve current visibility statuses for:
# - non-ifc objects
# - type product
# - annotations (so we won't unhide other drawings)
ifc_file = tool.Ifc.get()
visibility_status: dict[bpy.types.Object, bool] = {}
for obj in bpy.data.objects:
element = tool.Ifc.get_entity(obj)
if not element:
hide = obj.hide_get()
elif element.is_a("IfcAnnotation"):
hide = True
elif element.is_a("IfcTypeProduct"):
hide = obj.hide_get()
else:
continue
visibility_status[obj] = hide
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_clear(select=False)
bpy.ops.bim.activate_status_filters(only_if_enabled=True)
elements = {e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))}
@@ -2235,10 +2230,15 @@ class ActivateModel(bpy.types.Operator):
tool.Geometry,
obj=obj,
representation=model,
should_reload=False,
is_global=True,
should_sync_changes_first=True,
)
tool.Blender.reset_object_visibility()
tool.Drawing.hide_all_drawing_collections()
# restore visibility after hide_view_clear()
for obj, hide_status in visibility_status.items():
obj.hide_set(hide_status)
tool.Blender.update_viewport()
bonsai.bim.handler.refresh_ui_data()
@@ -2335,8 +2335,9 @@ class ActivateDrawingBase(tool.Ifc.Operator):
dprops.active_drawing_id = self.drawing
dprops.drawing_styles.clear()
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
if tool.Drawing.is_camera_orthographic():
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
@@ -2349,22 +2350,10 @@ class ActivateDrawingBase(tool.Ifc.Operator):
camera = context.scene.camera
assert camera
camera_props = tool.Drawing.get_camera_props(camera)
# Check if this is a reflected ceiling camera and preserve its scale
camera_element = tool.Ifc.get_entity(camera)
is_reflected = False
if camera_element:
is_reflected = (
ifcopenshell.util.element.get_pset(camera_element, "EPset_Drawing", "TargetView")
== "REFLECTED_PLAN_VIEW"
)
if is_reflected and camera.scale != (-1, -1, -1):
camera.scale = (-1, -1, -1)
if camera_props.update_representation(camera.matrix_world):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
# Restore the scale after update if needed
if is_reflected:
camera.scale = (-1, -1, -1)
# See 6452 and 6478.
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
@@ -2840,13 +2829,8 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.schedules:
cls.poll_message_set("No schedule selected.")
return False
if not props.sheets:
cls.poll_message_set("No sheets available.")
return False
if not tool.Blender.get_user_data_dir():
cls.poll_message_set("BIM data directory not set.")
return False
return True
prefs = tool.Blender.get_addon_preferences()
return props.schedules and props.sheets and prefs.data_dir
def _execute(self, context):
props = tool.Drawing.get_document_props()
@@ -2913,13 +2897,8 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.references:
cls.poll_message_set("No reference selected.")
return False
if not props.sheets:
cls.poll_message_set("No sheets available.")
return False
if not tool.Blender.get_user_data_dir():
cls.poll_message_set("BIM data directory not set.")
return False
return True
bim_props = tool.Blender.get_bim_props()
return props.references and props.sheets and bim_props.data_dir
def _execute(self, context):
props = tool.Drawing.get_document_props()
@@ -3768,19 +3747,3 @@ class OpenDocumentationWebUi(bpy.types.Operator):
else:
bpy.ops.bim.open_web_browser(page="documentation")
return {"FINISHED"}
class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.exclude_annotation"
bl_label = "Exclude Annotation"
bl_description = "Excludes the automatic annotation reference from the drawing"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
if not (obj := bpy.context.scene.camera) or not (drawing := tool.Ifc.get_entity(obj)):
return
for obj in tool.Blender.get_selected_objects(include_active=False):
if (element := tool.Ifc.get_entity(obj)) and tool.Drawing.is_auto_annotation(element):
if referenced_element := tool.Drawing.get_annotation_element(element):
tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
@@ -465,12 +465,6 @@ class DocProperties(PropertyGroup):
return None
return tool.Ifc.get().by_id(drawing_id)
def get_active_target_view(self) -> Union[str, None]:
active_drawing = self.get_active_drawing()
if not active_drawing:
return None
return tool.Drawing.get_drawing_target_view(active_drawing)
def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
self.update_camera_resolution()
@@ -498,9 +492,6 @@ class BIMCameraProperties(PropertyGroup):
name="Linework Mode",
update=get_update_layer_callback("linework_mode", "LineworkMode"),
)
generate_material_layers: bpy.props.BoolProperty(
name="Generate Material Layers", description="Generate material layer linework in drawings", default=True
)
fill_mode: EnumProperty(
items=[
("NONE", "None", "Disable filling areas seen in projection"),
@@ -747,11 +738,6 @@ class BIMTextProperties(PropertyGroup):
name="Font Size",
)
newline_at: IntProperty(name="Newline At")
reverse_list: BoolProperty(name="Reverse List", description="Reverses the order of any list.", default=False)
list_separator: StringProperty( # pyright: ignore[reportRedeclaration]
name="List Separator",
description="Text used to separate lists. Uses a comma (, ) if empty.",
)
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
name="Symbol",
description="Symbol from symbols.svg to use for this text.",
@@ -768,8 +754,6 @@ class BIMTextProperties(PropertyGroup):
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
font_size: str
newline_at: int
reverse_list: bool
list_separator: str
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
custom_symbol: str
@@ -806,8 +790,6 @@ class BIMTextProperties(PropertyGroup):
"FontSize": float(self.font_size),
"Newline_At": int(self.newline_at),
"Symbol": self.get_symbol(),
"Reverse_List": self.reverse_list,
"List_Separator": self.list_separator or ", ",
}
return text_data
@@ -862,19 +844,6 @@ class BIMAnnotationProperties(PropertyGroup):
)
is_adding_type: bpy.props.BoolProperty(default=False)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
tag_rotation_mode: bpy.props.EnumProperty(
name="Tag Rotation Mode",
description="How to orient the tag relative to the tagged object",
items=[
("NONE", "No Rotation", "Keep tag in default orientation"),
("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"),
("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"),
("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"),
("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"),
("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"),
],
default="NONE",
)
if TYPE_CHECKING:
object_type: str
@@ -839,12 +839,6 @@ class SvgWriter:
symbol = tool.Drawing.get_annotation_symbol(element)
newline_at = tool.Drawing.get_newline_at(element)
# Get reverse_list and list_separator from EPset_Annotation
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
reverse_list = pset_data.get("Reverse_List", False)
list_separator = pset_data.get("List_Separator") or ", "
template_text_fields = []
if symbol:
symbol_transform = self.get_symbol_transform(text_position_svg_str, angle, text_obj)
@@ -861,7 +855,7 @@ class SvgWriter:
# NOTE: zip makes sure that we iterate over the shortest list
for field, text_literal in zip(template_text_fields, text_literals):
field.text = tool.Drawing.replace_text_literal_variables(
text_literal.Literal, product or element, reverse_list, list_separator
text_literal.Literal, product or element
)
field.attrib["class"] = classes_str
@@ -881,9 +875,7 @@ class SvgWriter:
line_number = 0
for text_literal in text_literals:
text = tool.Drawing.replace_text_literal_variables(
text_literal.Literal, product or element, reverse_list, list_separator
)
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element)
text_tags = self.create_text_tag(
text,
text_position_svg,
+1 -13
View File
@@ -99,13 +99,12 @@ class BIM_PT_camera(Panel):
row = self.layout.row()
row.prop(props, "linework_mode")
row = self.layout.row()
row.prop(props, "generate_material_layers")
if props.linework_mode == "OPENCASCADE":
row = self.layout.row()
row.prop(props, "fill_mode")
row = self.layout.row()
row.prop(props, "cut_mode")
row = self.layout.row()
row.prop(props, "width")
row = self.layout.row()
@@ -194,7 +193,6 @@ class BIM_PT_element_filters(Panel):
text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
row.label(text=text, icon="FILTER")
row.operator("bim.exclude_annotation", icon="REMOVE", text="")
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
@@ -592,10 +590,6 @@ class BIM_PT_text(Panel):
row.prop(props, "font_size")
row = self.layout.row(align=True)
row.prop(props, "newline_at")
row = self.layout.row(align=True)
row.prop(props, "reverse_list")
row = self.layout.row(align=True)
row.prop(props, "list_separator")
row = self.layout.row(align=True)
row.prop(props, "symbol")
@@ -654,12 +648,6 @@ class BIM_PT_text(Panel):
row = self.layout.row(align=True)
row.label(text="Newline_At")
row.label(text=str(text_data["Newline_At"]))
row = self.layout.row(align=True)
row.label(text="Reverse_List")
row.label(text=str(text_data["Reverse_List"]))
row = self.layout.row(align=True)
row.label(text="List_Separator")
row.label(text=str(text_data["List_Separator"]))
for literal_data in text_data["Literals"]:
box = self.layout.box()
@@ -180,6 +180,9 @@ def create_annotation_occurrence(context):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if obj.data and not relating_type_rep:
@@ -228,6 +231,7 @@ class AnnotationToolUI:
def draw_type_selection_interface(cls):
# shared by both sidebar and header
object_type = cls.props.object_type
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "object_type", text="")
@@ -247,9 +251,6 @@ class AnnotationToolUI:
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
row = cls.layout.row(align=True)
row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")
row.prop(cls.props, "tag_rotation_mode", text="")
add_layout_hotkey_operator(
cls.layout,
"Bulk Tag",
@@ -301,8 +302,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return
related_objects = bpy.context.selected_objects
created_objects = []
for related_object in related_objects:
obj = core.add_annotation(
tool.Ifc,
@@ -315,15 +314,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
),
enable_editing=False,
)
tool.Drawing.setup_annotation_object(obj, object_type, related_object, props.tag_rotation_mode)
created_objects.append(obj)
# Select the created annotation objects
bpy.ops.object.select_all(action="DESELECT")
for obj in created_objects:
obj.select_set(True)
if created_objects:
bpy.context.view_layer.objects.active = created_objects[-1]
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
def hotkey_S_A(self):
if bpy.ops.bim.add_annotation.poll():
@@ -353,5 +344,4 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
continue
related_object = tool.Ifc.get_object(related_product)
rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode)
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
@@ -109,9 +109,8 @@ def block_scale(scene: bpy.types.Scene) -> None:
if obj.type == "CAMERA":
camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
# Only update if scale isn't already (-1, -1, -1)
if obj.scale != (-1, -1, -1):
obj.scale = (-1, -1, -1)
obj.scale = (-1, -1, -1)
obj.rotation_euler = (0.0, 0.0, math.radians(180))
else:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
@@ -427,7 +427,9 @@ class SwitchRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_definition_id: bpy.props.IntProperty()
should_reload: bpy.props.BoolProperty()
disable_opening_subtractions: bpy.props.BoolProperty()
should_switch_all_meshes: bpy.props.BoolProperty()
@classmethod
def poll(cls, context):
@@ -463,6 +465,9 @@ class SwitchRepresentation(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=self.should_reload,
is_global=self.should_switch_all_meshes,
should_sync_changes_first=True,
)
@@ -716,6 +721,9 @@ class UpdateParametricRepresentation(bpy.types.Operator):
tool.Geometry,
obj=obj,
representation=tool.Ifc.get().by_id(props.ifc_definition_id),
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if show_representation_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
@@ -858,10 +866,6 @@ class OverrideDelete(bpy.types.Operator):
objects_to_remove = context.selected_objects
self.process_arrays(context)
# Track aggregates before deleting their parts
aggregates_to_check = self.track_aggregates(objects_to_remove)
clear_active_object = True
for i, obj in enumerate(objects_to_remove, 1):
@@ -886,28 +890,13 @@ class OverrideDelete(bpy.types.Operator):
continue
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
continue
if element.is_a("IfcGridAxis"):
# Deleting the last W axis is OK
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
):
self.report(
{"INFO"}, "The last grid axis of a grid cannot be deleted. Delete the grid instead."
)
continue
if tool.Drawing.is_auto_annotation(element):
self.report({"INFO"}, "References cannot be deleted. Exclude the referenced element instead.")
continue
return {"FINISHED"}
tool.Geometry.delete_ifc_object(obj)
elif tool.Geometry.is_representation_item(obj):
tool.Geometry.delete_ifc_item(obj)
else:
bpy.data.objects.remove(obj)
# Delete empty aggregates after deleting their parts
self.delete_empty_aggregates(aggregates_to_check)
for opening in tool.Model.get_model_props().openings:
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
bpy.data.objects.remove(opening.obj)
@@ -940,49 +929,6 @@ class OverrideDelete(bpy.types.Operator):
data["old_file"].redo()
tool.Ifc.set(data["new_file"])
def track_aggregates(self, objects_to_remove):
"""Track aggregates that contain objects being deleted"""
aggregates_to_check = set()
for obj in objects_to_remove:
if not tool.Blender.is_valid_data_block(obj):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregates_to_check.add(aggregate)
return aggregates_to_check
def delete_empty_aggregates(self, aggregates_to_check):
"""Delete aggregates that now have no parts"""
deleted_aggregates = []
for aggregate in aggregates_to_check:
# Check if aggregate still exists (might have been deleted already)
try:
aggregate.id()
except:
continue
related_objects = ifcopenshell.util.element.get_parts(aggregate)
if len(related_objects) == 0:
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
deleted_aggregates.append(aggregate_name)
aggregate_obj = tool.Ifc.get_object(aggregate)
if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj):
tool.Geometry.delete_ifc_object(aggregate_obj)
# Show info message if aggregates were deleted
if deleted_aggregates:
if len(deleted_aggregates) == 1:
self.report(
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
)
else:
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
def process_arrays(self, context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
selected_objects = set(context.selected_objects)
@@ -1481,13 +1427,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
annotations = get_assignments(element)
container = ifcopenshell.util.element.get_container(element)
original_data[group][index] = {
"Name": pset["Name"],
"Aggregate_Index": pset["Aggregate_Index"],
"Assignment": annotations,
"Container": container,
}
original_data[group][index] = {"Name": str(pset["Aggregate_Index"]), "Assignment": annotations}
parts = ifcopenshell.util.element.get_parts(element)
if parts:
@@ -1530,22 +1470,13 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
index = pset["Index"]
if index == 0:
obj.name = pset["Name"] + "_" + str(original_data[group][index]["Aggregate_Index"])
obj.name = pset["Name"]
ifc_file = tool.Ifc.get()
ifcopenshell.api.pset.edit_pset(
ifc_file,
ifc_file.by_id(pset["id"]),
properties={"Aggregate_Index": int(original_data[group][index]["Aggregate_Index"])},
properties={"Aggregate_Index": int(original_data[group][index]["Name"])},
)
bonsai.core.spatial.assign_container(
tool.Ifc,
tool.Collector,
tool.Spatial,
container=original_data[group][index]["Container"],
element_obj=obj,
)
for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)):
tool.Collector.assign(tool.Ifc.get_object(part))
assignments = original_data[group][index]["Assignment"]
if assignments:
assign_to_annotations(obj, assignments)
@@ -1958,6 +1889,9 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=self.target,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
@@ -3035,7 +2969,6 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
item_mesh = bpy.data.meshes.new("tmp")
tool.Ifc.link(item, item_mesh)
item_obj = bpy.data.objects.new("tmp", item_mesh)
tool.Geometry.lock_scale(item_obj)
tool.Geometry.name_item_object(item_obj, item)
item_obj.matrix_world = obj.matrix_world
bpy.context.collection.objects.link(item_obj)
@@ -157,6 +157,8 @@ class BIM_PT_representations(Panel):
icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH",
text="",
)
op.should_switch_all_meshes = True
op.should_reload = True
op.ifc_definition_id = representation["id"]
op.disable_opening_subtractions = False
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
+1 -1
View File
@@ -41,7 +41,7 @@ class SolarData:
@classmethod
def sun_position(cls):
return tool.Blender.get_addon("sun_position")
return tool.Blender.get_sun_position_addon()
@classmethod
def sites(cls):
@@ -202,7 +202,7 @@ class RadianceRender(bpy.types.Operator):
print(f"Camera position: {camera_position}")
print(f"Camera direction: {camera_direction}")
# sun_position = tool.Blender.get_addon("sun_position")
# sun_position = tool.Blender.get_sun_position_addon()
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
# sun_pos_props.time,
# sun_pos_props.latitude,
+1 -1
View File
@@ -40,7 +40,7 @@ from bpy.types import PropertyGroup
from bonsai.bim.module.light.data import SolarData
from bonsai.bim.module.light.decorator import SolarDecorator
sun_position = tool.Blender.get_addon("sun_position")
sun_position = tool.Blender.get_sun_position_addon()
now = datetime.datetime.now()
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
+15 -18
View File
@@ -323,23 +323,20 @@ class ObjectMaterialData:
@classmethod
def total_thickness(cls):
if not cls.material:
return
layers = []
if cls.material.is_a("IfcMaterialLayerSetUsage"):
layers = cls.material.ForLayerSet.MaterialLayers
elif cls.material.is_a("IfcMaterialLayerSet"):
layers = cls.material.MaterialLayers
if not layers:
return
thickness = sum([l.LayerThickness for l in layers or []])
prefs = tool.Blender.get_addon_preferences()
assert bpy.context.scene
unit_system = bpy.context.scene.unit_settings.system
precision = None
if unit_system == "IMPERIAL":
precision = prefs.doc.imperial_precision
return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
if cls.material:
layers = []
if cls.material.is_a("IfcMaterialLayerSetUsage"):
layers = cls.material.ForLayerSet.MaterialLayers
elif cls.material.is_a("IfcMaterialLayerSet"):
layers = cls.material.MaterialLayers
thickness = sum([l.LayerThickness for l in layers or []])
prefs = tool.Blender.get_addon_preferences()
assert bpy.context.scene
unit_system = bpy.context.scene.unit_settings.system
precision = None
if unit_system == "IMPERIAL":
precision = prefs.doc.imperial_precision
return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
@classmethod
def set_item_name(cls) -> Union[str, None]:
@@ -407,7 +404,7 @@ class ObjectMaterialData:
material = cls.material
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
return []
return [m.Name for m in material.MaterialConstituents or [] if m.Name]
return [m.Name for m in material.MaterialConstituents if m.Name]
@classmethod
def is_type_material_overridden(cls) -> bool:
@@ -254,10 +254,6 @@ class BIM_PT_object_material(Panel):
active_object = bpy.context.active_object
self.layerset_bounds(box, active_object, location="Top_Exterior")
if not ObjectMaterialData.data["set_items"]:
row = box.row()
row.label(text="No Materials Found")
for set_item in ObjectMaterialData.data["set_items"]:
if (
len(self.props.material_set_item_profile_attributes)
@@ -361,10 +357,6 @@ class BIM_PT_object_material(Panel):
active_object = bpy.context.active_object
self.layerset_bounds(box, active_object, location="Top_Interior")
if not ObjectMaterialData.data["set_items"]:
row = box.row()
row.label(text="No Materials Found")
for set_item in ObjectMaterialData.data["set_items"]:
material_name = set_item["material"]
material_id = set_item["material_id"]
@@ -193,6 +193,9 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=False,
)
@@ -221,6 +224,9 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
@@ -1226,7 +1226,7 @@ class BoundingBoxDecorator:
def find_closest_trihedron(corners, edges, region, rv3d):
min_y = float("inf")
best_origin = 0
best_origin = None
for idx, corner in enumerate(corners):
screen_co = location_3d_to_region_2d(region, rv3d, corner)
if screen_co is not None and screen_co.y < min_y:
+11 -7
View File
@@ -95,17 +95,15 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
representation_data["part_of_product"] = None
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
if fallback_material := (int(props.lining_material) or int(props.framing_material) or int(props.glazing_material)):
materials = {
"Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material),
"Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material),
}
if props.transom_thickness:
materials["Glazing"] = tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material)
ifcopenshell.api.material.set_shape_aspect_constituents(
ifc_file,
element=element,
context=body,
materials=materials,
materials={
"Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material),
"Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material),
"Glazing": tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material),
},
)
elif material := ifcopenshell.util.element.get_material(element):
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
@@ -145,6 +143,9 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
tool.Geometry,
obj=obj,
representation=ifcopenshell.util.representation.get_representation(element, active_context),
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# type attributes
@@ -578,6 +579,9 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = False
@@ -186,6 +186,9 @@ class FilledOpeningGenerator:
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
@@ -246,6 +249,9 @@ class FilledOpeningGenerator:
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
def generate_opening_from_filling(
@@ -414,6 +420,9 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=building_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -789,6 +798,9 @@ class CloneOpening(Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -557,33 +557,7 @@ def get_generic_product_preview_data(context, relating_type):
mouse_point.z = snap_obj.matrix_world.translation.z
if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)):
container_obj = tool.Ifc.get_object(container)
mouse_point.z = container_obj.location.z
obj_type = tool.Ifc.get_object(relating_type)
subcontexts = tool.Drawing.get_active_drawing_subcontexts()
if not subcontexts:
subcontexts = [("Model", "Body", "MODEL_VIEW")]
active_context = tool.Geometry.get_active_representation_context(obj_type)
active_context_params = tool.Geometry.get_subcontext_parameters(active_context)
for subcontext in subcontexts:
if subcontext == active_context_params:
break
representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext)
if representation:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj_type,
representation,
)
context.view_layer.update()
break
if obj_type.data:
data = ItemDecorator.get_obj_data(obj_type)
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
+21 -19
View File
@@ -327,21 +327,9 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
props.relating_type_id = str(self.relating_type_id)
building_obj = None
if len(context.selected_objects) == 1 and context.active_object:
building_obj = context.active_object
building_element = tool.Ifc.get_entity(building_obj)
self.container = None
self.container_obj = None
if (
building_obj
and building_element
and (container := ifcopenshell.util.element.get_container(building_element))
):
self.container = container
self.container_obj = tool.Ifc.get_object(container)
elif container := tool.Root.get_default_container():
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
@@ -406,6 +394,9 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif self.representation_template == "EXTRUSION":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -419,9 +410,17 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return
building_obj = None
if len(context.selected_objects) == 1 and context.active_object:
building_obj = context.active_object
building_element = tool.Ifc.get_entity(building_obj)
mesh = bpy.data.meshes.new(name="Instance")
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(relating_type, instance_class), mesh)
@@ -447,6 +446,9 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# Update required as core.type.assign_type may change obj.data
@@ -485,15 +487,12 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
building_obj
and building_element
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering", "IfcElementAssembly"]
and instance_class in ["IfcWindow", "IfcDoor"]
):
# TODO For now we are hardcoding windows and doors as a prototype
tool.Model.add_filled_opening(building_obj, obj)
if instance_class in ["IfcWindow", "IfcDoor"]:
# TODO For now we are hardcoding windows and doors as a prototype
tool.Model.add_filled_opening(building_obj, obj)
else:
if self.container_obj:
bonsai.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, container=self.container, element_obj=obj
)
if props.rl_mode == "BOTTOM":
obj.location.z = self.container_obj.location.z - tool.Blender.get_object_bounding_box(obj)["min_z"]
elif props.rl_mode == "CONTAINER":
@@ -729,4 +728,7 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
+14 -3
View File
@@ -98,8 +98,7 @@ class DumbProfileGenerator:
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
coords = (vec1, vec2)
if profile := self.create_profile_from_2_points(coords):
profiles.append(profile)
profiles.append(self.create_profile_from_2_points(coords))
return profiles, is_polyline_closed
def derive_from_cursor(self) -> bpy.types.Object:
@@ -167,6 +166,9 @@ class DumbProfileGenerator:
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
@@ -181,7 +183,7 @@ class DumbProfileGenerator:
) -> ProfileFrom2PointsReturn:
self.direction = coords[1] - coords[0]
length = self.direction.length
if round(length, 4) < 0.001:
if round(length, 4) < 0.1:
return
data: dict[str, Any] = {"coords": coords}
@@ -565,6 +567,9 @@ class DumbProfileJoiner:
tool.Geometry,
obj=obj,
representation=new_body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
if element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting"):
@@ -1031,6 +1036,9 @@ def disable_editing_extrusion_axis(context):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -1092,6 +1100,9 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.context.view_layer.update()
+3 -34
View File
@@ -373,12 +373,7 @@ class BIMStairProperties(PropertyGroup):
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
self["nosing_length"] = 0
def update_custom_tread_lock(self, context: bpy.types.Context) -> None:
"""When lock is enabled, sync custom treads with tread_run"""
if self.custom_tread_lock:
self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock")
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
is_editing: bpy.props.BoolProperty(default=False)
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
@@ -412,12 +407,6 @@ class BIMStairProperties(PropertyGroup):
default="CONCRETE",
update=validate_nosing_value,
)
custom_tread_lock: bpy.props.BoolProperty(
name="Lock First/Last Treads to Tread Run",
description="When enabled, first and last treads automatically use the Tread Run value",
default=True,
update=update_custom_tread_lock,
)
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
name="Custom First / Last Treads Widths",
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
@@ -453,7 +442,6 @@ class BIMStairProperties(PropertyGroup):
top_slab_depth: float
has_top_nib: bool
stair_type: str
custom_tread_lock: bool
custom_first_last_tread_run: tuple[float, float]
nosing_length: float
nosing_depth: float
@@ -492,11 +480,8 @@ class BIMStairProperties(PropertyGroup):
}
stair_kwargs.update(generic_props)
# If locked, use tread_run for both first and last treads
if self.custom_tread_lock:
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
else:
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
# defined here to appear last in UI
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
if not convert_to_project_units:
return stair_kwargs
@@ -504,24 +489,8 @@ class BIMStairProperties(PropertyGroup):
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
return stair_kwargs
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
"""Get props including custom_tread_lock for saving to IFC"""
stair_kwargs = self.get_props_kwargs(convert_to_project_units, stair_type)
# Add the lock state for IFC storage (after getting base kwargs to avoid passing to generate function)
stair_kwargs["custom_tread_lock"] = self.custom_tread_lock
return stair_kwargs
def set_props_kwargs_from_ifc_data(self, kwargs):
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
# Determine lock state based on whether custom treads match tread_run
# If custom_tread_lock wasn't saved (old files), infer it from the data
if "custom_tread_lock" not in kwargs:
custom_treads = kwargs.get("custom_first_last_tread_run", (0.0, 0.0))
tread_run = kwargs.get("tread_run", 0.3)
# Lock is off if either custom tread differs from tread_run and is not 0
kwargs["custom_tread_lock"] = not any(ct != 0.0 and ct != tread_run for ct in custom_treads)
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
@@ -538,6 +538,9 @@ def cancel_editing_railing_path(context: bpy.types.Context) -> set[str]:
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -167,6 +167,9 @@ class DumbSlabGenerator:
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
obj.matrix_world = obj.matrix_world @ Matrix.Rotation(self.x_angle, 4, "X")
@@ -357,6 +360,9 @@ class DumbSlabPlaner:
tool.Geometry,
obj=obj,
representation=new_rep,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bonsai.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
@@ -380,6 +386,9 @@ class DumbSlabPlaner:
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@@ -579,6 +588,9 @@ class EditSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -620,6 +632,9 @@ def disable_editing_extrusion_profile(context):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -752,6 +767,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# Only certain classes should have a footprint
+2 -4
View File
@@ -188,8 +188,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_stair_props(obj)
ifc_file = tool.Ifc.get()
# Use the special method that includes custom_tread_lock for IFC storage
stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
stair_data = props.get_props_kwargs(convert_to_project_units=True)
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair")
@@ -242,8 +241,7 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
assert element
props = tool.Model.get_stair_props(obj)
# Use the special method that includes custom_tread_lock for IFC storage
data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
data = props.get_props_kwargs(convert_to_project_units=True)
props.is_editing = False
regenerate_stair_mesh(obj)
tool.Model.add_body_representation(obj)
+2 -27
View File
@@ -302,38 +302,13 @@ class BIM_PT_stair(bpy.types.Panel):
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
if prop_name == "custom_tread_lock":
continue
prop_value = getattr(props, prop_name)
# Special handling for custom_first_last_tread_run
if prop_name == "custom_first_last_tread_run":
# Draw the lock toggle
row_lock = self.layout.row(align=True)
lock_text = (
"Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
)
row_lock.prop(
props,
"custom_tread_lock",
text=lock_text,
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
)
# Only show the custom values input if unlocked
if not props.custom_tread_lock:
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
else:
self.layout.prop(props, prop_name)
if prop_name == "height": # Weak but we just want to insert this inside props drawing
row_length = self.layout.row(align=True)
row_length.prop(props, "total_length_target")
@@ -575,7 +550,7 @@ class BIM_PT_door(bpy.types.Panel):
row = prop_with_search(self.layout, props, "lining_material")
tool.Model.draw_material_ui_select(row, props.lining_material)
row = prop_with_search(self.layout, props, "framing_material", text="Panel Framing Material")
row = prop_with_search(self.layout, props, "framing_material", text="Panel Material")
tool.Model.draw_material_ui_select(row, props.framing_material)
if props.transom_thickness:
row = prop_with_search(self.layout, props, "glazing_material")
@@ -517,6 +517,9 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# Object rotation
@@ -1021,6 +1024,9 @@ class DumbWallGenerator:
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
@@ -123,6 +123,9 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None:
tool.Geometry,
obj=obj,
representation=ifcopenshell.util.representation.get_representation(element, active_context),
should_reload=True,
is_global=True,
should_sync_changes_first=True,
)
# type attributes
@@ -492,6 +495,9 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = False
@@ -155,7 +155,6 @@ class CreateProject(bpy.types.Operator):
tool.Ifc, tool.Georeference, tool.Project, tool.Spatial, schema=props.export_schema, template=template
)
tool.Blender.register_toolbar()
tool.Loader.set_unit_scale(ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()))
def rollback(self, data):
IfcStore.file = None
@@ -2522,7 +2521,6 @@ class FlipClippingPlane(bpy.types.Operator):
obj = context.active_object
if obj in tool.Project.get_project_props().clipping_planes_objs:
obj.rotation_euler[0] += radians(180)
obj.rotation_euler[0] %= radians(360)
context.view_layer.update()
return {"FINISHED"}
+3 -3
View File
@@ -450,9 +450,9 @@ class BIMProjectProperties(PropertyGroup):
self.mvd = header_data.mvd
self.author_name = header_data.author_name
self.author_email = header_data.author_email
self.organisation_name = header_data.organisation_name
self.organisation_email = header_data.organisation_email
self.authorisation = header_data.authorisation
self.organisation_name = header_data.organization_name
self.organisation_email = header_data.organization_email
self.authorisation = header_data.authorization
if TYPE_CHECKING:
is_editing: bool
+1 -2
View File
@@ -653,5 +653,4 @@ class BIM_PT_purge(Panel):
row = layout.row(align=True)
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
merge_op.object_type = object_type
row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = object_type
+16 -10
View File
@@ -242,11 +242,14 @@ class BIM_PT_object_psets(Panel):
@classmethod
def poll(cls, context):
return (
(obj := tool.Blender.get_active_object())
and (element := tool.Ifc.get_entity(obj))
and element.is_a("IfcObjectDefinition")
)
if not (obj := context.active_object):
return False
ifc_id = tool.Blender.get_ifc_definition_id(obj)
if not ifc_id:
return False
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
def draw(self, context):
if not ObjectPsetsData.is_loaded:
@@ -385,11 +388,14 @@ class BIM_PT_object_qtos(Panel):
@classmethod
def poll(cls, context):
return (
(obj := tool.Blender.get_active_object())
and (element := tool.Ifc.get_entity(obj))
and element.is_a("IfcObjectDefinition")
)
if not (obj := context.active_object):
return False
ifc_id = tool.Blender.get_ifc_definition_id(obj)
if not ifc_id:
return False
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
def draw(self, context):
if not ObjectQtosData.is_loaded:
+26 -2
View File
@@ -317,7 +317,6 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
)
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
element = core.assign_class(
tool.Ifc,
tool.Collector,
@@ -327,9 +326,16 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
predefined_type=predefined_type,
should_add_representation=False,
)
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
else:
@@ -573,6 +579,9 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if not tool.Ifc.get_entity(props.representation_obj):
bpy.data.objects.remove(props.representation_obj)
@@ -592,6 +601,9 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "EXTRUSION":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -638,6 +650,9 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template in ("LAYERSET_AXIS2", "LAYERSET_AXIS3"):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -748,6 +763,9 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "EDGE":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -760,6 +778,9 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "FACE":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -772,6 +793,9 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.context.view_layer.update() # Ensures obj.matrix_world is correct
+1 -1
View File
@@ -101,7 +101,7 @@ class ColourByPropertyData:
elif pset.endswith("BaseQuantities"):
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
else:
keys.extend([f'"{pset}"."{name}"' for name in properties.keys() if name != "id"])
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
results = [(k, k, "") for k in keys]
return default + results
@@ -105,7 +105,6 @@ classes = (
operator.RemoveWorkTime,
operator.ReorderTask,
operator.SaveAnimationColorScheme,
operator.SelectTaskElements,
operator.SelectTaskRelatedInputs,
operator.SelectTaskRelatedProducts,
operator.SelectUnassignedWorkScheduleProducts,
@@ -243,9 +243,6 @@ class SequenceData:
data["NestingIndex"] = None
for rel in task.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(task)
data["TotalInputs"] = len(tool.Sequence.get_task_inputs(task))
data["TotalOutputs"] = len(tool.Sequence.get_task_outputs(task))
data["TotalElements"] = data["TotalInputs"] + data["TotalOutputs"]
cls.data["tasks"][task.id()] = data
@classmethod
@@ -1281,17 +1281,6 @@ class DisableEditingSequence(bpy.types.Operator):
return {"FINISHED"}
class SelectTaskElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_task_elements"
bl_label = "Select All Task Elements"
bl_options = {"REGISTER", "UNDO"}
task: bpy.props.IntProperty()
def _execute(self, context):
core.select_task_inputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
core.select_task_outputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
class SelectTaskRelatedProducts(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_task_related_products"
bl_label = "Select All Output Products"
+3 -15
View File
@@ -75,8 +75,6 @@ def update_active_task_index(self, context):
bonsai.bim.module.pset.data.refresh()
if self.editing_task_type == "SEQUENCE":
tool.Sequence.load_task_properties()
if self.should_auto_select:
tool.Sequence.select_active_task_elements(task)
def update_active_task_outputs(self, context):
@@ -487,9 +485,6 @@ class BIMWorkScheduleProperties(PropertyGroup):
other_columns: EnumProperty(
items=[
("Controls.Calendar", "Calendar", ""),
("Controls.TotalElements", "Total Elements", "The total number of assigned elements to this task"),
("Controls.TotalInputs", "Total Inputs", "The total number of input elements to this task"),
("Controls.TotalOutputs", "Total Outputs", "The total number of input elements to this task"),
],
name="Special Columns",
)
@@ -523,13 +518,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_input_index: IntProperty(name="Active Task Input Index")
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
active_task_output_index: IntProperty(name="Active Task Output Index")
show_nested_outputs: BoolProperty(
name="Show Nested Task Elements", default=False, update=update_active_task_outputs
)
show_nested_resources: BoolProperty(
name="Show Nested Task Elements", default=False, update=update_active_task_resources
)
show_nested_inputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_inputs)
show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs)
show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources)
show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs)
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
@@ -540,9 +531,6 @@ class BIMWorkScheduleProperties(PropertyGroup):
filter_by_active_schedule: BoolProperty(
name="Filter By Active Schedule", default=False, update=update_filter_by_active_schedule
)
should_auto_select: BoolProperty(
name="Auto Select", description="Auto select elements when task is selected", default=False
)
if TYPE_CHECKING:
work_schedule_predefined_types: str
+14 -36
View File
@@ -290,21 +290,13 @@ class BIM_PT_work_schedules(Panel):
row2 = self.layout.row(align=True)
row2.alignment = "RIGHT"
col = row2.column()
row_ = col.row(align=True)
op = row_.operator("bim.select_task_elements", icon="RESTRICT_SELECT_OFF", text="Select Elements")
op.task = task.ifc_definition_id
row_.prop(self.props, "should_auto_select", icon="OBJECT_HIDDEN", text="")
col = row2.column()
row_ = col.row(align=True)
row_.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
row_.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
row_.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
row_.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
row2.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
ifc_definition_id
)
row_.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
row2.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
ifc_definition_id
)
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
@@ -724,10 +716,11 @@ class BIM_PT_task_icom(Panel):
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
op.related_object = input_id
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
op.task = task.ifc_definition_id
row2.prop(self.props, "show_nested_inputs", icon="OUTLINER", text="")
row2 = col.row()
row2.prop(self.props, "show_nested_inputs", text="Show Nested")
row2 = col.row()
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
@@ -751,7 +744,8 @@ class BIM_PT_task_icom(Panel):
op.related_object_type = "RESOURCE"
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
row2.prop(self.props, "show_nested_resources", icon="OUTLINER", text="")
row2 = col.row()
row2.prop(self.props, "show_nested_resources", text="Show Nested")
row2 = col.row()
row2.template_list(
@@ -779,10 +773,10 @@ class BIM_PT_task_icom(Panel):
output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
op.relating_product = output_id
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="")
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select")
op.task = task.ifc_definition_id
row2.prop(self.props, "show_nested_outputs", icon="OUTLINER", text="")
row2 = col.row()
row2.prop(self.props, "show_nested_outputs", text="Show Nested")
row2 = col.row()
row2.template_list(
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
@@ -899,7 +893,7 @@ class BIM_UL_tasks(UIList):
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
split2.prop(item, "name", emboss=False, text="")
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task, item.ifc_definition_id)
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task)
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
row.prop(
@@ -966,7 +960,6 @@ class BIM_UL_tasks(UIList):
row: bpy.types.UILayout,
item: Optional[bpy.types.PropertyGroup] = None,
task: Optional[dict[str, Any]] = None,
ifc_definition_id: int = 0,
*,
header: bool = False,
) -> None:
@@ -1006,21 +999,6 @@ class BIM_UL_tasks(UIList):
row.label(text=item.derived_calendar + "*")
else:
row.label(text=item.calendar or "-")
elif column.name == "Controls.TotalInputs":
if header:
row.label(text="# Inputs")
else:
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalInputs"]))
elif column.name == "Controls.TotalOutputs":
if header:
row.label(text="# Outputs")
else:
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalOutputs"]))
elif column.name == "Controls.TotalElements":
if header:
row.label(text="# Elements")
else:
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalElements"]))
else:
ifc_class, name = column.name.split(".")
if header:
+5 -5
View File
@@ -148,14 +148,14 @@ class SpatialDecompositionData:
elif tool.Ifc.get_schema() == "IFC4X3":
results = {
"IfcBridge": ["IfcBridge", "IfcBridgePart", "IfcSpace"],
"IfcBridgePart": ["IfcBridgePart", "IfcSpace"],
"IfcBridgePart": ["IfcSpace"],
"IfcBuilding": ["IfcBuilding", "IfcBuildingStorey", "IfcSpace"],
"IfcBuildingStorey": ["IfcBuildingStorey", "IfcSpace"],
"IfcExternalSpatialElement": ["IfcExternalSpatialElement"],
"IfcFacility": ["IfcFacility", "IfcFacilityPartCommon", "IfcSpace"],
"IfcFacilityPartCommon": ["IfcFacilityPartCommon", "IfcSpace"],
"IfcFacilityPartCommon": ["IfcSpace"],
"IfcMarineFacility": ["IfcMarineFacility", "IfcMarinePart", "IfcSpace"],
"IfcMarinePart": ["IfcMarinePart", "IfcSpace"],
"IfcMarinePart": ["IfcSpace"],
"IfcProject": [
"IfcBridge",
"IfcBuilding",
@@ -168,9 +168,9 @@ class SpatialDecompositionData:
"IfcSpace",
],
"IfcRailway": ["IfcRailway", "IfcRailwayPart", "IfcSpace"],
"IfcRailwayPart": ["IfcRailwayPart", "IfcSpace"],
"IfcRailwayPart": ["IfcSpace"],
"IfcRoad": ["IfcRoad", "IfcRoadPart", "IfcSpace"],
"IfcRoadPart": ["IfcRoadPart", "IfcSpace"],
"IfcRoadPart": ["IfcSpace"],
"IfcSite": [
"IfcBridge",
"IfcBuilding",
@@ -70,7 +70,7 @@ class GridDecorator:
grid_props = tool.Spatial.get_grid_props()
for axis in grid_props.grid_axes:
if not (obj := axis.obj) or obj.visible_get() is False:
if not (obj := axis.obj) or obj.hide_get() == True:
continue
if obj.select_get() and context.mode != "OBJECT":
continue
@@ -125,22 +125,21 @@ class GridDecorator:
unselected_edges = []
grid_props = tool.Spatial.get_grid_props()
for axis in grid_props.grid_axes:
if not (obj := axis.obj) or obj.visible_get() is False:
continue
if obj.select_get():
if context.mode != "OBJECT":
continue
edges = selected_edges
verts = selected_verts
else:
edges = unselected_edges
verts = unselected_verts
i = len(verts)
edges.append([i, i + 1])
matrix_world = obj.matrix_world
v1 = matrix_world @ obj.data.vertices[0].co
v2 = matrix_world @ obj.data.vertices[1].co
verts.extend([v1, v2])
if (obj := axis.obj) and obj.hide_get() == False:
if obj.select_get():
if context.mode != "OBJECT":
continue
edges = selected_edges
verts = selected_verts
else:
edges = unselected_edges
verts = unselected_verts
i = len(verts)
edges.append([i, i + 1])
matrix_world = obj.matrix_world
v1 = matrix_world @ obj.data.vertices[0].co
v2 = matrix_world @ obj.data.vertices[1].co
verts.extend([v1, v2])
if unselected_verts:
self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges)
@@ -550,7 +550,9 @@ class ToggleGrids(bpy.types.Operator):
is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"})
def execute(self, context):
tool.Spatial.set_grid_visibility(self.is_visible)
for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"):
if obj := tool.Ifc.get_object(element):
obj.hide_set(not self.is_visible)
return {"FINISHED"}
@@ -562,5 +564,11 @@ class ToggleSpatialElements(bpy.types.Operator):
is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"})
def execute(self, context):
tool.Spatial.set_space_visibility(self.is_visible)
if tool.Ifc.get().schema == "IFC2X3":
elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
else:
elements = tool.Ifc.get().by_type("IfcSpatialElement")
for element in elements:
if obj := tool.Ifc.get_object(element):
obj.hide_set(not self.is_visible)
return {"FINISHED"}
+1 -1
View File
@@ -224,7 +224,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
is_visible: BoolProperty(
name="Is Visible",
description="Show or hide spatial elements, such as buildings, sites, etc",
default=False,
default=True,
update=update_spatial_is_visible,
)
container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"})
@@ -25,7 +25,6 @@ classes = (
operator.StopIfcTesterWebapp,
operator.OpenIfcTesterWebapp,
operator.SelectRequirement,
operator.ColorSpecefication,
operator.SelectFailedEntities,
operator.ExportBcf,
prop.FailedEntities,
@@ -135,7 +135,6 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
try:
request_id = data.get("id")
ids_string = data.get("ids")
props = tool.Tester.get_tester_props()
if not request_id:
await self.emit("error", {"error": "No request ID provided"}, room=sid)
@@ -177,7 +176,7 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
json_report = json_reporter.to_string()
# HTML report
html_reporter = ifctester.reporter.Html(ids, hide_skipped=props.hide_skipped_specs)
html_reporter = ifctester.reporter.Html(ids)
html_reporter.report()
html_report = html_reporter.to_string()
@@ -268,7 +267,7 @@ class ExecuteIfcTester(bpy.types.Operator):
start = time.time()
if props.generate_html_report:
engine = ifctester.reporter.Html(specs, props.hide_skipped_specs)
engine = ifctester.reporter.Html(specs)
engine.report()
output_path = output.as_posix()
engine.to_file(output_path)
@@ -338,21 +337,9 @@ class StartIfcTesterWebapp(bpy.types.Operator):
websocket_server_thread = threading.Thread(target=run_websocket_server, daemon=True)
websocket_server_thread.start()
py_version = sys.version_info
bonsai_lib_path = (
Path(bpy.utils.user_resource("EXTENSIONS"))
/ ".local"
/ "lib"
/ f"python{py_version.major}.{py_version.minor}"
/ "site-packages"
)
env = os.environ.copy()
env["BONSAI_LIB_PATH"] = str(bonsai_lib_path)
env["BONSAI_VERSION"] = tool.Blender.get_bonsai_version()
# Start the Flask server as subprocess
webapp_process = subprocess.Popen(
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)], env=env
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)]
)
# Update properties
@@ -485,7 +472,6 @@ class SelectRequirement(bpy.types.Operator):
props.n_entities = len(failed_entities)
props.has_entities = True if props.n_entities > 0 else False
props.failed_entities.clear()
props.active_requirement_index = self.req_index
for e in failed_entities:
new_entity = props.failed_entities.add()
@@ -535,39 +521,6 @@ class SelectFailedEntities(bpy.types.Operator):
return {"FINISHED"}
class ColorSpecefication(bpy.types.Operator):
"""Colors all entities red that failed each test and color all entities yellow that failed some tests"""
bl_idname = "bim.color_specification"
bl_label = "Color Failed Entities"
bl_options = {"REGISTER", "UNDO"}
spec_index: bpy.props.IntProperty()
def execute(self, context):
props = context.scene.IfcTesterProperties
report = tool.Tester.report
failures = [
{e["id"] for e in requirement["failed_entities"]} for requirement in report[self.spec_index]["requirements"]
]
failed_all_ids = set.intersection(*failures)
failed_some_ids = set.union(*failures) - failed_all_ids
if props.flag:
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].shading.color_type = "OBJECT"
area.spaces[0].shading.show_xray = True
for obj in context.scene.objects:
ifc_id = tool.Blender.get_ifc_definition_id(obj)
if ifc_id in failed_all_ids:
obj.color = (1, 0, 0, 1)
elif ifc_id in failed_some_ids:
obj.color = (1, 1, 0, 1)
else:
obj.color = (1, 1, 1, 1)
return {"FINISHED"}
class ExportBcf(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_bcf"
bl_label = "Export BCF"
@@ -71,8 +71,6 @@ class IfcTesterProperties(PropertyGroup):
generate_ods_report: BoolProperty(default=False, name="Generate ODS report", options=set())
flag: BoolProperty(default=False, name="Flag Failed Entities", options=set())
active_specification_index: IntProperty(name="Active Specification Index", update=update_active_specification_index)
active_requirement_index: IntProperty(name="Active Requirement Index")
old_index: IntProperty(name="", default=0)
active_failed_entity_index: IntProperty(name="Active Failed Entity Index")
specifications: CollectionProperty(name="Specifications", type=Specification)
@@ -82,7 +80,6 @@ class IfcTesterProperties(PropertyGroup):
webapp_server_port: IntProperty(name="Webapp Server Port", default=0)
webapp_is_running: BoolProperty(default=False, name="Webapp Is Running", options=set())
websocket_server_port: IntProperty(name="WebSocket Server Port", default=0)
hide_skipped_specs: BoolProperty(default=False, name="Hide skipped Specifications", options=set())
if TYPE_CHECKING:
specs: MultipleFileSelect
@@ -92,7 +89,6 @@ class IfcTesterProperties(PropertyGroup):
generate_ods_report: bool
flag: bool
active_specification_index: int
active_requirement_index: int
old_index: int
active_failed_entity_index: int
specifications: bpy.types.bpy_prop_collection_idprop[Specification]
+3 -57
View File
@@ -20,8 +20,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from bonsai.bim.ui import draw_multiline_text
import bpy
from bpy.types import Panel, UIList
import bonsai.tool as tool
@@ -62,8 +60,7 @@ class BIM_PT_tester(Panel):
row.prop(props, "generate_ods_report")
row = self.layout.row()
row.prop(props, "flag")
row = self.layout.row()
row.prop(props, "hide_skipped_specs")
if not tool.Ifc.get() or not props.should_load_from_memory:
row = self.layout.row(align=True)
props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)")
@@ -98,16 +95,13 @@ class BIM_PT_tester(Panel):
"active_specification_index",
)
self.draw_editable_ui(context)
self.draw_editable_ui()
row = self.layout.row()
row.operator("bim.export_bcf", text="Export BCF", icon="EXPORT")
def draw_editable_ui(self, context: bpy.types.Context) -> None:
def draw_editable_ui(self) -> None:
props = tool.Tester.get_tester_props()
specification = TesterData.data["specification"]
is_skipped = specification["total_checks"] == 0 and specification["cardinality"] == "optional"
if props.hide_skipped_specs and is_skipped:
return
n_requirements = len(specification["requirements"])
@@ -116,18 +110,8 @@ class BIM_PT_tester(Panel):
row.label(
text=f'Passed: {specification["total_checks_pass"]}/{specification["total_checks"]} ({specification["percent_checks_pass"]}%)'
)
row = self.layout.row()
if specification.get("instructions"):
row.label(text="Instructions:")
box = self.layout.box()
column = box.column(align=True)
draw_multiline_text(column, specification.get("instructions"), context=context)
row = self.layout.row()
row.label(text=f"Requirements ({n_requirements}):")
if props.flag:
op = row.operator("bim.color_specification", text="", icon="COLOR")
op.spec_index = props.active_specification_index
box = self.layout.box()
for i, requirement in enumerate(specification["requirements"]):
row = box.row(align=True)
@@ -145,16 +129,6 @@ class BIM_PT_tester(Panel):
and props.n_entities > 0
and len(props.failed_entities) > 0
):
requirement = specification["requirements"][props.active_requirement_index]
metadata = requirement.get("metadata")
if metadata and metadata.get("@instructions"):
row = self.layout.row()
row.label(text="Instructions:")
box = self.layout.box()
column = box.column(align=True)
draw_multiline_text(column, metadata.get("@instructions"), context=context)
row = self.layout.row()
row.label(text=f"Failed entities [{props.n_entities}]:")
self.layout.template_list(
@@ -183,34 +157,6 @@ class BIM_UL_tester_specifications(UIList):
row.label(text=item.name, icon="CHECKMARK" if item.status else "CANCEL")
row.label(text=item.description)
def filter_items(self, context: bpy.types.Context, data: IfcTesterProperties, propname: str):
items = getattr(data, propname)
filter_flags = [self.bitflag_filter_item] * len(items)
props = tool.Tester.get_tester_props()
if props.hide_skipped_specs:
for idx, item in enumerate(items):
report = tool.Tester.report[idx]
if report["total_checks"] != 0:
filter_flags[idx] |= self.bitflag_filter_item
else:
filter_flags[idx] &= ~self.bitflag_filter_item
filter_name = self.filter_name
if filter_name:
name_filtered = bpy.types.UI_UL_list.filter_items_by_name(
filter_name,
self.bitflag_filter_item,
items,
"name",
)
if len(name_filtered) == len(filter_flags):
for idx, flag in enumerate(name_filtered):
if flag == 0:
filter_flags[idx] &= ~self.bitflag_filter_item
return filter_flags, []
class BIM_UL_tester_failed_entities(UIList):
def draw_item(
@@ -121,6 +121,9 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=new_active_representation,
should_reload=False,
is_global=False,
should_sync_changes_first=False,
)
return {"FINISHED"}
+8 -5
View File
@@ -34,11 +34,14 @@ class BIM_PT_type(Panel):
@classmethod
def poll(cls, context):
return (
(obj := tool.Blender.get_active_object())
and (element := tool.Ifc.get_entity(obj))
and (element.is_a("IfcObject") or element.is_a("IfcTypeObject"))
)
if not context.active_object:
return False
element = tool.Ifc.get_entity(context.active_object)
if not element:
return False
if not element.is_a("IfcProduct") and not element.is_a("IfcTypeProduct"):
return True
return True
def draw(self, context):
if not TypeData.is_loaded:
@@ -148,6 +148,11 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
# Don't sync changes because object has an opening,
# therefore bim.update_representaiton wouldn't work either way.
should_sync_changes_first=False,
)
tool.Geometry.lock_scale(voided_obj)
@@ -191,6 +196,9 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=building_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.unlock_scale_object_with_openings(obj)
tool.Geometry.clear_cache(element)
+7 -26
View File
@@ -40,7 +40,6 @@ from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
from bonsai.bim.module.pset.prop import IfcProperty
from typing import Optional, TYPE_CHECKING, Literal
from natsort import natsorted
import textwrap
if TYPE_CHECKING:
@@ -374,6 +373,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
description="Path to create and store temporary files. If left blank, a system default will be used.",
subtype="DIR_PATH",
)
spatial_elements_unselectable: BoolProperty(
name="Make Spatial Elements Unselectable By Default",
default=True,
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences",
)
decorations_colour: bpy.props.FloatVectorProperty(
name="Decorations Color", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
@@ -499,6 +503,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
activate_workspace: bool
should_setup_toolbar: bool
should_play_chaching_sound: bool
spatial_elements_unselectable: bool
tmp_dir: str
decorations_colour: tuple[float, float, float, float]
decorator_color_selected: tuple[float, float, float, float]
@@ -560,6 +565,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "should_setup_toolbar")
layout.prop(self, "should_use_snap")
layout.prop(self, "should_play_chaching_sound")
layout.prop(self, "spatial_elements_unselectable")
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "occurrence_name_style")
@@ -1492,31 +1498,6 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
url_op.uri = url
def draw_multiline_text(
layout: bpy.types.UILayout,
text: str,
*,
context: bpy.types.Context | None = None,
) -> None:
"""Render a read-only text box that wraps long text."""
assert layout
region_width = 200
if context and context.region:
region_width = context.region.width
approximate_char_width = 7 # Empirical average width for Blender UI font (px)
wrap_width = max(20, int(region_width / approximate_char_width))
for paragraph in text.splitlines():
if not paragraph:
layout.label(text="")
continue
for line in textwrap.wrap(paragraph, width=wrap_width):
layout.label(text=line)
class BIM_PT_decorators_overlay(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "HEADER"
+38 -32
View File
@@ -39,7 +39,7 @@ def disable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) ->
def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.synchronise_ifc_and_text_attributes(obj)
drawing.update_text_size_pset(obj)
drawing.update_text_annotation_properties(obj)
drawing.update_newline_at_and_symbol(obj)
drawing.disable_editing_text(obj)
@@ -108,6 +108,10 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
def regenerate_sheet(
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
) -> Union[list[tool.Drawing.SheetWarningType], None]:
warnings = drawing.validate_sheet_files(sheet)
if warnings:
return warnings
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
assert titleblock_uri
@@ -316,7 +320,6 @@ def duplicate_drawing(
) -> ifcopenshell.entity_instance:
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
new_drawing = ifc.run("root.copy_class", product=drawing)
drawing_tool.clear_annotation_relationships(new_drawing)
drawing_tool.copy_representation(drawing, new_drawing)
drawing_tool.set_name(new_drawing, drawing_name)
group = drawing_tool.get_drawing_group(new_drawing)
@@ -437,7 +440,8 @@ def add_annotation(
drawing_tool.show_decorations()
obj = drawing_tool.create_annotation_object(drawing, object_type)
element = ifc.get_entity(obj)
if not element: # Brand new annotation
# TODO: element is never None?
if not element:
relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None
element = drawing_tool.run_root_assign_class(
obj=obj,
@@ -472,44 +476,46 @@ def sync_references(
if not drawing_tool.has_annotation(drawing):
return
if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))):
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))
if not context:
return
group = drawing_tool.get_drawing_group(drawing)
potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing)
for element in potential_reference_elements:
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
drawing_tool.sync_object_placement(obj)
for element in drawing_tool.get_group_elements(group):
if not drawing_tool.is_auto_annotation(element):
continue
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
drawing_tool.sync_object_placement(obj)
if not (reference_element := drawing_tool.get_assigned_product(element)):
if obj := ifc.get_object(element):
drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element)
continue
for reference_element in drawing_tool.get_potential_reference_elements(drawing):
reference_obj = ifc.get_object(reference_element)
if reference_element not in potential_reference_elements:
# It was auto created, so it makes sense to auto delete
if obj := ifc.get_object(element):
drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element)
elif not drawing_tool.regenerate_reference_annotation(drawing, element, reference_element, context):
if obj := ifc.get_object(element):
drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element)
annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element)
for reference_element in potential_reference_elements:
if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
should_delete_existing_annotation = False
should_create_annotation = False
# remove annotation only if the reference object was changed
# otherwise we rely on the existing annotation
if annotation:
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
should_delete_existing_annotation = True
if should_delete_existing_annotation or not annotation:
should_create_annotation = True
if should_delete_existing_annotation:
annotation_obj = ifc.get_object(annotation)
if annotation_obj:
drawing_tool.delete_object(annotation_obj)
ifc.run("root.remove_product", product=annotation)
if should_create_annotation:
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
if annotation:
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
ifc.run("group.assign_group", group=group, products=[annotation])
collector.assign(ifc.get_object(annotation))
if reference_obj and ifc.is_moved(reference_obj):
drawing_tool.sync_object_placement(reference_obj)
if reference_obj and ifc.is_edited(reference_obj):
drawing_tool.sync_object_representation(reference_obj)
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
drawing.select_assigned_product(context)
+19 -2
View File
@@ -115,9 +115,24 @@ def switch_representation(
geometry: type[tool.Geometry],
obj: bpy.types.Object,
representation: ifcopenshell.entity_instance,
should_reload: bool = True,
is_global: bool = True,
should_sync_changes_first: bool = False,
apply_openings: bool = True,
) -> None:
"""Switch obj representation (even if not yet assigned to that object. See #2766.)"""
"""Function can switch to representation that wasn't yet assigned to that object. See #2766.
:param should_sync_changes_first: sync ifc representation with current state of `obj.data`
:param should_reload: reload `obj.data` from ifc representation
:param is_global: replace mesh data for all users of `obj.data`, not just `obj`
"""
if should_sync_changes_first and ifc.is_edited(obj) and not geometry.is_box_representation(representation):
representation_id = geometry.get_representation_id(representation)
geometry.run_geometry_update_representation(obj=obj)
if not geometry.does_representation_id_exist(representation_id):
return
if not geometry.get_object_data(obj) and geometry.is_text_literal(representation):
return
@@ -127,7 +142,9 @@ def switch_representation(
geometry.reimport_element_representations(obj, representation, apply_openings=apply_openings)
def get_representation_ifc_parameters(geometry: type[tool.Geometry], obj: bpy.types.Object) -> None:
def get_representation_ifc_parameters(
geometry: type[tool.Geometry], obj: bpy.types.Object, should_sync_changes_first: bool = False
) -> None:
geometry.import_representation_parameters(geometry.get_object_data(obj))
-4
View File
@@ -72,10 +72,6 @@ def assign_class(
"""
Args:
context: is not optional if `should_add_representation` is True
TODO: Do NOT use should_add_representation. Because it internally calls
geometry.add_representation which is 1,000 lines of Blender -> IFC magic.
Instead, explicitly create a representation using tool.Geometry.
"""
if ifc.get_entity(obj):
return
+2 -2
View File
@@ -394,7 +394,7 @@ class Drawing:
def sync_object_placement(cls, obj): pass
def synchronise_ifc_and_text_attributes(cls, obj): pass
def update_embedded_svg_location(cls, uri, old_location, new_location): pass
def update_text_annotation_properties(cls, obj): pass
def update_newline_at_and_symbol(cls, obj): pass
def update_text_size_pset(cls, obj): pass
@@ -1092,7 +1092,7 @@ class Type:
def get_type_occurrences(cls, element_type): pass
def has_material_usage(cls, element): pass
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
def run_geometry_switch_representation(cls, obj=None, representation=None): pass
def run_geometry_switch_representation(cls, obj=None, representation=None, should_reload=None, is_global=None): pass
@interface
+12 -94
View File
@@ -477,8 +477,6 @@ class Blender(bonsai.core.tool.Blender):
def ensure_bin_in_path(cls) -> None:
"""Check 'bin' folder is in PATH, if not add for this session"""
bin_dir = str(Path(__file__).parent.parent.resolve() / "libs" / "bin")
if not os.path.isdir(bin_dir):
return # Maybe the user is using a system-wide Python package. See #7157.
current_path = os.environ["PATH"]
if bin_dir not in current_path:
os.environ["PATH"] = current_path + os.pathsep + bin_dir
@@ -1506,21 +1504,28 @@ class Blender(bonsai.core.tool.Blender):
return bpy.context.preferences.addons[blender_package_name].preferences
@classmethod
def get_addon(cls, name: str) -> Union[types.ModuleType, None]:
def get_sun_position_addon(cls) -> Union[types.ModuleType, None]:
# Check if it's installed as legacy Blender addon.
import importlib
try:
return importlib.import_module(name) # Legacy Blender addon
sun_position = importlib.import_module("sun_position")
except ImportError:
pass
sun_position = None
if sun_position:
return sun_position
for package_name in bpy.context.preferences.addons.keys():
if package_name.endswith(f".{name}"):
if package_name.endswith(".sun_position"):
try:
return importlib.import_module(package_name)
sun_position = importlib.import_module(package_name)
return sun_position
except ModuleNotFoundError:
pass
return sun_position
@classmethod
def get_sun_props(cls) -> Union[SunPosProperties, None]:
assert (scene := bpy.context.scene)
@@ -1854,93 +1859,6 @@ class Blender(bonsai.core.tool.Blender):
return unit_scale
@classmethod
def reset_object_visibility(cls):
override = cls.get_viewport_context()
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_clear(select=False)
@classmethod
def isolate_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects}
previously_active = bpy.context.view_layer.objects.active
override = cls.get_viewport_context()
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_clear(select=False)
bpy.ops.object.select_all(action="DESELECT")
for obj in objs:
obj.select_set(True)
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_set(unselected=True)
bpy.ops.object.select_all(action="DESELECT")
for name in previously_selected:
obj = bpy.data.objects.get(name)
if obj:
obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active
@classmethod
def sync_render_visibility(cls):
# Doing bpy.ops.object.hide_render_clear_all() or
# bpy.ops.object.isolate_type_render() is extremely slow.
# Hopefully this doesn't crash on Windows, it doesn't crash on Linux.
should_hides = [0 if obj.visible_get() else 1 for obj in bpy.data.objects]
should_hides = np.fromiter(should_hides, dtype=np.uint8, count=len(should_hides))
bpy.data.objects.foreach_set("hide_render", should_hides)
return # Otherwise...
# for obj in bpy.data.objects:
# if not obj.data:
# continue
# # For speed, check equality prior to change to prevent needless updates
# if (is_visible := obj.visible_get()) and obj.hide_render is True:
# obj.hide_render = False
# elif not is_visible and obj.hide_render is False:
# obj.hide_render = True
@classmethod
def hide_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects}
previously_active = bpy.context.view_layer.objects.active
override = cls.get_viewport_context()
bpy.ops.object.select_all(action="DESELECT")
for obj in objs:
obj.select_set(True)
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_set(unselected=False)
for name in previously_selected:
obj = bpy.data.objects.get(name)
if obj:
obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active
@classmethod
def show_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects}
previously_active = bpy.context.view_layer.objects.active
bpy.ops.object.select_all(action="DESELECT")
override = cls.get_viewport_context()
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_clear(select=True)
for obj in bpy.context.selected_objects:
if obj in objs:
obj.select_set(False)
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_set(unselected=False)
bpy.ops.object.select_all(action="DESELECT")
for name in previously_selected:
obj = bpy.data.objects.get(name)
if obj:
obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active
@classmethod
def validate_shader_batch_data(cls, pos: Any, indices: Optional[Any]) -> bool:
"""Validate shader batch data.
-12
View File
@@ -51,13 +51,9 @@ class Collector(bonsai.core.tool.Collector):
if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj)
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
if not tool.Spatial.get_grid_props().is_visible:
obj.hide_viewport = True
elif element.is_a("IfcGrid"):
if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj)
if not tool.Spatial.get_grid_props().is_visible:
obj.hide_viewport = True
if element.is_a("IfcProject"):
if tool.Geometry.is_locked(element):
@@ -69,12 +65,8 @@ class Collector(bonsai.core.tool.Collector):
collection = cls._create_project_child_collection("IfcTypeProduct")
cls.link_collection_object_safe(collection, obj)
elif element.is_a("IfcSpace"):
if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj)
collection = cls._create_project_child_collection("IfcSpace")
cls.link_collection_object_safe(collection, obj)
if not tool.Spatial.get_spatial_props().is_visible:
obj.hide_viewport = True
elif element.is_a("IfcStructuralItem"):
collection = cls._create_project_child_collection("IfcStructuralItem")
cls.link_collection_object_safe(collection, obj)
@@ -98,8 +90,6 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
if not tool.Spatial.get_spatial_props().is_visible:
obj.hide_viewport = True
elif (
tool.Ifc.get_schema() != "IFC2X3"
and element.is_a("IfcSpatialElement")
@@ -111,8 +101,6 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
if not tool.Spatial.get_spatial_props().is_visible:
obj.hide_viewport = True
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
if collection := cls._create_own_collection(obj):
cls.link_collection_object_safe(collection, obj)

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