Compare commits

..

7 Commits

427 changed files with 5480 additions and 16573 deletions
-8
View File
@@ -1,8 +0,0 @@
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+12 -36
View File
@@ -11,7 +11,7 @@ jobs:
matrix:
include:
- os: macos
runner: macos-14
runner: macos-13
arch: x64
oldarch:
- os: macos
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -34,10 +34,14 @@ 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
# preinstalled: xz, cmake
brew install git bison autoconf automake libffi findutils
echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH
@@ -51,44 +55,16 @@ jobs:
[ -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 }}
key: ${GITHUB_WORKFLOW}-${{ matrix.os }}
- name: Run Build Script
shell: bash
run: |
if [ "${{ matrix.os }}" == "macos" ]; then
DARWIN_C_SOURCE=-D_DARWIN_C_SOURCE
fi
if [ "${{ matrix.arch }}" == "x64" ]; then
arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
MAC_INTEL=-mac-cross-compile-intel
# 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
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
if: always()
uses: actions/upload-artifact@v5
with:
name: build-logs-osx-${{ matrix.arch }}
path: |
build.log
build/*/*/*/logs/*.log
build/*/*/*/build/ifcopenshell/**/CMakeCache.txt
retention-days: 30
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
- name: Pack Dependencies
run: |
@@ -139,7 +115,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+24 -58
View File
@@ -9,75 +9,41 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
path: IfcOpenShell
- name: Checkout Build Repository
uses: actions/checkout@v5
- 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@v5
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@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+10 -22
View File
@@ -16,7 +16,7 @@ jobs:
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils xz byacc
findutils xz
python3 -m pip install typing_extensions
git config --global --add safe.directory '*'
@@ -29,12 +29,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -47,27 +47,15 @@ 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
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ${GITHUB_WORKFLOW}-rockylinux8-x64
- name: Run Build Script
shell: bash
run: |
set -o pipefail
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
with:
name: build-logs-rocky
path: |
build.log
build/*/*/logs/*.log
retention-days: 30
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
- name: Pack Dependencies
run: |
@@ -118,7 +106,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+10 -22
View File
@@ -16,7 +16,7 @@ jobs:
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils xz byacc
findutils xz
python3 -m pip install typing_extensions
git config --global --add safe.directory '*'
@@ -29,12 +29,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -47,27 +47,15 @@ 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
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ${GITHUB_WORKFLOW}-rockylinux8-x64
- name: Run Build Script
shell: bash
run: |
set -o pipefail
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
with:
name: build-logs-rocky-arm64
path: |
build.log
build/*/*/logs/*.log
retention-days: 30
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
- name: Pack Dependencies
run: |
@@ -118,7 +106,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+52 -14
View File
@@ -9,15 +9,16 @@ 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
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: _deps-vs2022-x64-installed
@@ -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
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
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,8 +81,37 @@ 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@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5 # https://github.com/actions/checkout
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2 # https://github.com/actions/checkout
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+5 -9
View File
@@ -9,15 +9,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Action - checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4.2.2
- name: Action - install python
uses: actions/setup-python@v6
uses: actions/setup-python@v5.3.0
with:
python-version: "3.9"
- name: Action - install python
uses: actions/setup-python@v6
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
@@ -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,13 @@ 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
continue-on-error: true
- name: Final check
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
+3 -3
View File
@@ -54,8 +54,8 @@ jobs:
short_name: macosm1,
}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
@@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout bonsai_unstable_repo repository
uses: actions/checkout@v5
uses: actions/checkout@v2
with:
repository: IfcOpenShell/bonsai_unstable_repo
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+2 -2
View File
@@ -43,8 +43,8 @@ jobs:
short_name: macosm1,
}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -37,8 +37,8 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
@@ -24,7 +24,7 @@ jobs:
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
@@ -21,7 +21,7 @@ jobs:
date: ${{ steps.date.outputs.date }}
verdate: ${{ steps.verdate.outputs.verdate }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set env
run: echo ok go
@@ -76,7 +76,7 @@ jobs:
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
submodules: recursive
+8 -8
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
@@ -76,7 +76,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
# Artifact name
name: ifcos-artifacts
@@ -89,31 +89,31 @@ jobs:
name: Docker Build, Tag, Push
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
lfs: true
- name: Download
uses: actions/download-artifact@v6.0.0
uses: actions/download-artifact@v4.1.7
with:
# Artifact name
name: ifcos-artifacts
path: artifacts/
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v1
-
name: Login to Dockerhub
uses: docker/login-action@v3
uses: docker/login-action@v1
with:
username: aecgeeks
password: ${{ secrets.DOCKER_HUB_TOKEN }}
-
name: Build container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v2
with:
context: artifacts
repository: aecgeeks/ifcopenshell
@@ -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",
@@ -34,10 +34,6 @@ jobs:
name: "Linux 64bit",
short_name: linux64
}
- {
name: "Linux ARM 64bit",
short_name: linuxarm64
}
- {
name: "MacOS Intel 64bit",
short_name: macos64
@@ -47,10 +43,10 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+3 -3
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",
@@ -38,10 +38,10 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -25,8 +25,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
-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@v5
- name: Checkout ifctester_org_static_html
uses: actions/checkout@v5
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
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+35 -69
View File
@@ -35,12 +35,12 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.11
@@ -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: |
@@ -64,28 +63,22 @@ jobs:
libboost-regex-dev \
libboost-system-dev \
libboost-thread-dev \
libpcre3-dev libxml2-dev \
swig libpcre3-dev libxml2-dev \
libtbb-dev nlohmann-json3-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-dev libcgal-dev libeigen3-dev
- name: ccache
# TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released
# to update hardcoded references to commits in some other workflows.
# Then we can switch back to 1.2 in all actions.
uses: hendrikmuhs/ccache-action@v1.2.19
uses: hendrikmuhs/ccache-action@v1
with:
key: ubuntu-22.04-${{ runner.arch }}
key: ${GITHUB_WORKFLOW}
# 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 \
@@ -100,50 +93,15 @@ jobs:
..
sudo make -j$(nproc) install
# OpenCOLLADA is ancient, but we still have it in the main build.
# So adding it to CI to catch any breakages.
- name: build OpenCOLLADA
run: |
git clone https://github.com/KhronosGroup/OpenCOLLADA
cd OpenCOLLADA
git checkout v1.6.68
patch -p1 --batch --forward -i ../nix/patches/opencollada/pr622_and_disable_subdirs.patch
patch -p1 --batch --forward -i ../nix/patches/opencollada/allow_static_libraries_config_on_unix.patch
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_FLAGS_INIT="-fPIC"
sudo make -j$(nproc) install
# Ubuntu has `swig` package, but we build it to match the version we use in the main build.
# To avoid failing tests when checking stub generation.
- name: build swig
run: |
# Remove default swig to avoid conflicts.
sudo apt remove --purge swig swig4.0
sudo apt-get install -y libpcre2-dev bison
git clone https://github.com/swig/swig
cd swig
git checkout v4.1.0
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
sudo make -j$(nproc) install
- name: Build ifcopenshell
run: |
echo $Python3_ROOT_DIR
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 \
@@ -152,31 +110,39 @@ 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 \
-DUSE_MMAP=On \
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
-DCOLLADA_SUPPORT=Off \
"-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 \
-DROCKSDB_INCLUDE_DIR=/usr/local/include \
-DROCKSDB_LIBRARY_DIR=/usr/local/lib \
-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: |
@@ -186,7 +152,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
@@ -11,10 +11,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v2
with:
python-version: '3.x'
+2 -2
View File
@@ -9,10 +9,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v2
with:
python-version: '3.x'
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
@@ -34,7 +34,7 @@ jobs:
uses: actions/configure-pages@v5
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v3
with:
path: src/pyodide/demo-app/
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install C++ dependencies
+6 -6
View File
@@ -17,7 +17,6 @@
# General Python residue
__pycache__
*.py.bak
venv
# Visual Studio Code files
.vscode
@@ -79,6 +78,8 @@ src/bonsai/bonsai/translations.py
# bonsai test temp files
src/bonsai/test/files/temp
src/bonsai/test/files/basic.ifc.cache.blend
src/bonsai/test/files/basic.ifc.cache.sqlite
# bonsai data
src/bonsai/bonsai/bim/data/build/
@@ -86,7 +87,9 @@ src/bonsai/bonsai/bim/data/gantt/index.html
src/bonsai/bonsai/bim/data/gantt/jsgantt.js
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
src/bonsai/bonsai/bim/data/webui/static/js/jquery.min.js
src/bonsai/bonsai/bim/data/webui/running_pid.json
src/bonsai/drawings
src/bonsai/layouts
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
@@ -106,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/
+3
View File
@@ -11,6 +11,9 @@
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
path = src/ifcopenshell-python/test/Sample-BIM-Files
url = https://github.com/IfcOpenShell/ids-test-files
[submodule "src/ifcconvert/cityjson"]
path = src/ifcconvert/cityjson
url = https://github.com/IfcOpenShell/ifc-to-cityjson
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
path = docs/cpp-api/assets/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git
+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:
+1 -1
View File
@@ -1 +1 @@
0.8.5
0.8.4
+792 -117
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -17,6 +17,7 @@
"IFCXML_SUPPORT": "ON",
"HDF5_SUPPORT": "ON",
"SCHEMA_VERSIONS": "4x3_add2",
"CITYJSON_SUPPORT": "OFF",
"CMAKE_GENERATOR_PLATFORM": "",
"CMAKE_GENERATOR_TOOLSET": ""
}
-42
View File
@@ -1,42 +0,0 @@
#
# Input variables:
# - `CGAL_INCLUDE_DIR`
# - `CGAL_LIBRARY_DIR`
# - `GMP_INCLUDE_DIR`
# - `GMP_LIBRARY_DIR`
# - `MPFR_INCLUDE_DIR`
# - `MPFR_LIBRARY_DIR`
# If input variables are not specified, try to find HDF5 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `CGAL_INCLUDE_DIR`
#
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
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 CONFIG REQUIRED)
if(NOT CGAL_DIR)
message(
FATAL_ERROR
"CGAL_SUPPORT enabled, but CGAL_INCLUDE_DIR wasn't provided and CGAL package couldn't be found."
)
endif()
message(STATUS "CGAL: found config at '${CGAL_DIR}'.")
endif()
add_definitions(-DIFOPSH_WITH_CGAL)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
-27
View File
@@ -1,27 +0,0 @@
#
# Input variables:
# - `EIGEN_DIR`
# If input variables are not specified, try to find Eigen3 config.
# Input variables could also be provided as environment variables.
#
# Output targets:
# - `Eigen3::Eigen`
UNIFY_ENVVARS_AND_CACHE(EIGEN_DIR)
if(EXISTS "${EIGEN_DIR}")
# Mimic Eigen3Config.cmake target.
add_library(Eigen3::Eigen INTERFACE IMPORTED)
target_include_directories(Eigen3::Eigen INTERFACE "${EIGEN_DIR}")
else()
find_package(Eigen3 CONFIG)
if(Eigen3_DIR)
message(STATUS "Eigen3: found config at '${Eigen3_DIR}'.")
else()
message(
FATAL_ERROR
"EIGEN_DIR is not provided or provided folder doesn't exist (current value: '${EIGEN_DIR}'). "
"Also couldn't find Eigen3 as a package."
)
endif()
endif()
-109
View File
@@ -1,109 +0,0 @@
#
# Input variables:
# - `HDF5_INCLUDE_DIR`
# - `HDF5_LIBRARY_DIR`
# - `HDF5_LIBRARIES`
# If input variables are not specified, try to find HDF5 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `HDF5_INCLUDE_DIR`
# - `HDF5_LIBRARY_DIR`
# - `HDF5_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
# To avoid cyclic calls to this file
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No HDF5 include directory specified")
else()
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
endif()
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No HDF5 library directory specified")
else()
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
endif()
if(HDF5_LIBRARY_DIR)
# result of the HDF5 ctest package
# Find zlib using cmake find_library. How should this be implemented?
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
if("$ENV{CONDA_BUILD}" STREQUAL "")
# result of the HDF5 ctest package
if(WIN32)
set(zlib_post lib)
set(lib_ext lib)
else()
set(lib_ext a)
endif()
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(debug_postfix "_debug")
endif()
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
)
else()
message(STATUS "Packaging hdf5 and zlib for conda distribution")
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
# macOS
set(zlib_post libz)
set(lib_ext dylib)
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
)
else()
# linux and windows
# Find HDF5 package
find_package(HDF5 REQUIRED COMPONENTS C CXX)
# Find ZLIB package
find_package(ZLIB REQUIRED)
# Include directories
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
# Link libraries
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
endif()
endif()
endif()
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
# First try to find it as a config.
find_package(HDF5 CONFIG)
if(HDF5_DIR)
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
set(HDF5_LIBRARIES hdf5_cpp-static)
else()
# 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)
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)."
)
endif()
endif()
endif()
# Restore module path.
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
-49
View File
@@ -1,49 +0,0 @@
#
# Input variables:
# - `LIBXML2_INCLUDE_DIR`
# - `LIBXML2_LIBRARIES`
# If input variables are not specified, try to find LibXml2 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `LIBXML2_INCLUDE_DIR`
# - `LIBXML2_LIBRARIES`
#
# To avoid cyclic calls to this file
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
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.
find_package(LibXml2 QUIET CONFIG)
if(NOT LibXml2_FOUND)
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
find_package(LibXml2 REQUIRED)
else()
message(STATUS "Found LibXml2 config: ${LibXml2_DIR}")
endif()
if(TARGET LibXml2::LibXml2)
# Config mode already gives us the target
set(LIBXML2_LIBRARIES LibXml2::LibXml2)
get_target_property(LIBXML2_INCLUDE_DIR LibXml2::LibXml2 INTERFACE_INCLUDE_DIRECTORIES)
else()
# Module mode (Ubuntu)
set(LIBXML2_LIBRARIES ${LibXml2_LIBRARIES})
set(LIBXML2_INCLUDE_DIR ${LibXml2_INCLUDE_DIRS})
endif()
else()
find_package(LibXml2 REQUIRED)
endif()
if(MSVC AND NOT LibXml2_DIR)
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
endif()
# Restore module path.
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
-138
View File
@@ -1,138 +0,0 @@
#
# Input variables:
# - `OCC_INCLUDE_DIR`
# - `OCC_LIBRARY_DIR`
# If input variables are not specified, try to find OpenCASCADE config.
# Input variables could also be provided as environment variables.
#
# Output variables
# - `OpenCASCADE_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
if(OCC_INCLUDE_DIR)
set(OCC_INCLUDE_DIR ${OCC_INCLUDE_DIR} CACHE FILEPATH "Open CASCADE header files")
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
endif()
if(OCC_LIBRARY_DIR)
set(OCC_LIBRARY_DIR ${OCC_LIBRARY_DIR} CACHE FILEPATH "Open CASCADE library files")
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
endif()
if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
# OCE is not supported for find_package, because it's using a different name (`oce`)
# and also has an odd directory structure (install/lib/oce-0.18/*.cmake).
# find_package creates variables:
# - `OpenCASCADE_INCLUDE_DIR`
# - `OpenCASCADE_LIBRARIES`
find_package(OpenCASCADE CONFIG REQUIRED)
return()
endif()
# No specific paths specified, try to find package.
if(OCC_INCLUDE_DIR AND OCC_LIBRARY_DIR)
message(
STATUS
"Using provided OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') "
"and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}')."
)
# Parse OCC_VERSION_STRING.
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
REGEX "#define OCC_VERSION_MAJOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
REGEX "#define OCC_VERSION_MINOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
REGEX "#define OCC_VERSION_MAINTENANCE.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
else()
message(
FATAL_ERROR
"Couldn't find Open CASCADE installation. "
"Either both OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}') "
"must be specified or OpenCASCADE package should be discoverable. "
"If you're using OCE, then providing a package is not available "
"and you need to provide OCE_INCLUDE_DIR and OCE_LIBRARY_DIR directly."
)
endif()
set(
OpenCASCADE_LIBRARIES
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
TKFillet TKXSBase TKOffset TKHLR
# @todo investigate the exact conditions when this is necessary
TKBin
)
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OpenCASCADE_LIBRARIES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OpenCASCADE_LIBRARIES TKDESTEP TKDEIGES)
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
if(libTKernel)
message(STATUS "Required Open Cascade Library files found")
else()
message(
FATAL_ERROR
"Unable to find Open Cascade library files in OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}'), aborting"
)
endif()
if(MSVC)
add_definitions(-DHAVE_NO_DLL)
add_debug_variants(OpenCASCADE_LIBRARIES "${OpenCASCADE_LIBRARIES}" d)
endif()
if(WIN32)
# OCC might require linking to Winsock depending on the version and build configuration
list(APPEND OpenCASCADE_LIBRARIES ws2_32.lib)
endif()
# Make sure cross-referenced symbols between static OCC libraries get
# resolved. Also add thread and rt libraries.
get_filename_component(libTKernelExt ${libTKernel} EXT)
if("${libTKernelExt}" STREQUAL ".a")
set(OCCT_STATIC ON)
endif()
if(OCCT_STATIC)
find_package(Threads)
if(WASM_BUILD)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
# OpenCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
if(APPLE)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
set(OpenCASCADE_LIBRARIES -Wl,--start-group ${OpenCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
endif()
endif()
if(NOT APPLE AND NOT WIN32)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "rt")
endif()
if(NOT WIN32)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "dl")
endif()
endif()
add_library(OpenCASCADE_INTERFACE INTERFACE IMPORTED)
target_include_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_INCLUDE_DIR}")
target_link_libraries(OpenCASCADE_INTERFACE INTERFACE ${OpenCASCADE_LIBRARIES})
target_link_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_LIBRARY_DIR}")
set(OpenCASCADE_LIBRARIES OpenCASCADE_INTERFACE)
-118
View File
@@ -1,118 +0,0 @@
#
# Input variables:
# - `OPENCOLLADA_INCLUDE_DIR`
# - `OPENCOLLADA_LIBRARY_DIR`
# - `PCRE_LIBRARY_DIR`
# If input variables are not specified, try to find OpenCOLLADA config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `OPENCOLLADA_INCLUDE_DIR`
# - `OPENCOLLADA_LIBRARY_DIR`
# - `OPENCOLLADA_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
if(NOT OPENCOLLADA_INCLUDE_DIR AND NOT OPENCOLLADA_LIBRARY_DIR)
# If package is found, automatically sets
# OPENCOLLADA_INCLUDE_DIRS and OPENCOLLADA_LIBRARIES (list of targets, not paths).
find_package(OpenCOLLADA CONFIG)
if(OpenCOLLADA_DIR)
message(STATUS "Found OpenCOLLADA: '${OpenCOLLADA_DIR}'.")
set(OPENCOLLADA_FOUND TRUE)
else()
message(STATUS "OpenCOLLADA package not found, falling back to manual search.")
endif()
endif()
if(NOT OpenCOLLADA_DIR)
# Find OpenCOLLADA
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA include directory specified")
set(OPENCOLLADA_INCLUDE_DIR "/usr/include/opencollada" CACHE FILEPATH "OpenCOLLADA header files")
else()
set(OPENCOLLADA_INCLUDE_DIR "${OPENCOLLADA_INCLUDE_DIR}" CACHE FILEPATH "OpenCOLLADA header files")
endif()
if("${OPENCOLLADA_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA library directory specified")
find_library(OPENCOLLADA_FRAMEWORK_LIB NAMES OpenCOLLADAFramework
PATHS /usr/lib64/opencollada /usr/lib/opencollada /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib)
get_filename_component(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_FRAMEWORK_LIB} PATH)
endif()
find_library(OpenCOLLADAFramework NAMES OpenCOLLADAFramework OpenCOLLADAFrameworkd PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
if(OpenCOLLADAFramework)
message(STATUS "OpenCOLLADA library files found")
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA libraries. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
set(OPENCOLLADA_LIBRARY_DIR "${OPENCOLLADA_LIBRARY_DIR}" CACHE FILEPATH "OpenCOLLADA library files")
set(OPENCOLLADA_INCLUDE_DIRS "${OPENCOLLADA_INCLUDE_DIR}/COLLADABaseUtils" "${OPENCOLLADA_INCLUDE_DIR}/COLLADAStreamWriter")
find_file(COLLADASWStreamWriter_h "COLLADASWStreamWriter.h" ${OPENCOLLADA_INCLUDE_DIRS})
if(COLLADASWStreamWriter_h)
message(STATUS "OpenCOLLADA header files found")
set(OPENCOLLADA_FOUND TRUE)
set(OPENCOLLADA_LIBRARY_NAMES
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
OpenCOLLADAStreamWriter UTF buffer ftoa
)
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
endforeach()
if("${PCRE_LIBRARY_DIR}" STREQUAL "")
if(WIN32)
find_library(pcre_library NAMES pcre pcred PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
else()
find_library(pcre_library NAMES pcre PATHS ${OPENCOLLADA_LIBRARY_DIR})
endif()
get_filename_component(PCRE_LIBRARY_DIR ${pcre_library} PATH)
else()
find_library(pcre_library NAMES pcre pcred PATHS ${PCRE_LIBRARY_DIR} NO_DEFAULT_PATH)
endif()
if(pcre_library)
set(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_LIBRARY_DIR} ${PCRE_LIBRARY_DIR})
if(MSVC)
# Add release lib regardless whether release or debug found. Debug version will be appended below.
list(APPEND OPENCOLLADA_LIBRARIES "${PCRE_LIBRARY_DIR}/pcre.lib")
else()
list(APPEND OPENCOLLADA_LIBRARIES "${pcre_library}")
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find PCRE. "
"Disable COLLADA_SUPPORT or fix PCRE_LIBRARY_DIR path to proceed.")
endif()
if(MSVC)
add_debug_variants(OPENCOLLADA_LIBRARIES "${OPENCOLLADA_LIBRARIES}" d)
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA headers. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
endif(NOT OpenCOLLADA_DIR)
if(OPENCOLLADA_FOUND)
add_definitions(-DWITH_OPENCOLLADA)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
endif()
-54
View File
@@ -1,54 +0,0 @@
#
# Input variables:
# - `USD_INCLUDE_DIR`
# - `USD_LIBRARY_DIR`
# Input variables could also be provided as environment variables.
# TODO: Try to find USD config if varibales are not provided.
# TODO: does usd have a config file?
#
# Output variables:
# - `USD_LIBRARIES`
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
if("${USD_INCLUDE_DIR}" STREQUAL "")
find_path(USD_INCLUDE_DIR pxr.h
PATHS
/usr/include/pxr
/usr/local/include/pxr
REQUIRED
)
if(USD_INCLUDE_DIR)
message(STATUS "Found USD include files in: ${USD_INCLUDE_DIR}")
else()
message(FATAL_ERROR "Unable to find USD include directory, specify USD_INCLUDE_DIR manually.")
endif()
else()
set(USD_INCLUDE_DIR ${USD_INCLUDE_DIR} CACHE FILEPATH "USD header files")
message(STATUS "Looking for USD include files in: ${USD_INCLUDE_DIR}")
endif()
set(USD_LIBRARIES
usd_usd
usd_usdGeom
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_gf
)
find_library(USD_LIBRARY
NAMES ${USD_LIBRARIES}
PATHS ${USD_LIBRARY_DIR})
if(USD_LIBRARY)
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
link_directories(${USD_LIBRARY_DIR})
else()
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
endif()
add_definitions(-DWITH_USD)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
+1
View File
@@ -42,6 +42,7 @@ cmake -G "Ninja" ^
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
-D CITYJSON_SUPPORT:BOOL=OFF ^
../cmake
if errorlevel 1 exit 1
+1
View File
@@ -42,6 +42,7 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DBUILD_IFCGEOM:BOOL=ON \
-DBUILD_GEOMSERVER:BOOL=OFF \
-DBOOST_USE_STATIC_LIBS:BOOL=OFF \
-DCITYJSON_SUPPORT:BOOL=OFF \
./cmake
ninja
+161 -444
View File
File diff suppressed because it is too large Load Diff
@@ -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")
@@ -1,13 +0,0 @@
diff --git a/OpenCOLLADAConfig.cmake.in b/OpenCOLLADAConfig.cmake.in
index d765b108..6800c2a7 100644
--- a/OpenCOLLADAConfig.cmake.in
+++ b/OpenCOLLADAConfig.cmake.in
@@ -31,7 +31,7 @@ set(OPENCOLLADA_INCLUDE_DIRS
include("@PACKAGE_OPENCOLLADA_INST_CMAKECONFIG@/OpenCOLLADATargets.cmake")
# Set the library variable
-if(UNIX)
+if(0)
set(OPENCOLLADA_LIBRARIES
ftoa_shared
buffer_shared
-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)
"""
)
+7 -13
View File
@@ -9,8 +9,10 @@ extend-exclude = '''
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|src/ifc2ca/templates/*
|src/ifcconvert/cityjson/*
|src/svgfill
|src/exterior-shell-extractor
|src/pyodide
'''
[tool.pyright]
@@ -19,11 +21,12 @@ disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
# Define here general ruff settings,
# then they will be inherited by projects' .toml files.
# then they will be inherited projects .toml files.
# This allows using assuming different Python version for different projects.
[tool.ruff]
exclude = [
# Submodules.
"src/ifcconvert/cityjson",
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
@@ -35,7 +38,6 @@ exclude = [
"src/ifc2ca/templates",
]
[tool.ruff.lint]
preview = true
select = [
# Default Ruff rules.
# "E4", # imports
@@ -58,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 -5
View File
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=e8eb5e4
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
@@ -155,9 +155,6 @@ endif
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download six --dest=./wheels
# Required by drawing module
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
# Required by drawing module for markdown hyperlinks in annotations
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download markdown-it-py --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download mdurl --dest=./wheels
# Required by qto and drawing module
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
# Required by the BIM type manager thumbnail generator
+9 -40
View File
@@ -21,7 +21,7 @@ import bpy
import bpy.utils.previews
import importlib
from bpy_extras.io_utils import ImportHelper, ExportHelper
from . import handler, ui, prop, operator, gizmo
from . import handler, ui, prop, operator
from typing import Union
from collections.abc import Callable
@@ -148,9 +148,7 @@ classes = [
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.DocPreferences,
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
ui.GizmoPreferences,
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
ui.BIM_ADDON_preferences,
# Tabs panel
ui.BIM_PT_tabs,
# Project overview
@@ -204,14 +202,6 @@ classes = [
ui.BIM_PT_section_with_cappings,
ui.BIM_PT_decorators_overlay,
ui.BIM_PT_snappping,
# Gizmos
gizmo.GizmoArrow,
gizmo.GizmoCone,
gizmo.GizmoLock,
gizmo.GizmoArc,
gizmo.GizmoPen,
gizmo.GizmoValidate,
gizmo.GizmoCancel,
]
for mod in modules.values():
@@ -235,33 +225,14 @@ def on_register(scene):
is_registering = False
# Classes that need to be registered after modules (due to cross-module dependencies)
late_classes = (
ui.DefaultParameters, # Register before BIM_ADDON_preferences
ui.BIM_ADDON_preferences,
)
def register_classes(classes_to_register):
for cls in classes_to_register:
def register():
for cls in classes:
# Prevent crashes in Blender 4.4.0, see #6420.
if issubclass(cls, (ImportHelper, ExportHelper)):
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
bpy.utils.register_class(cls)
def unregister_classes(classes_to_unregister):
for cls in reversed(classes_to_unregister):
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
bpy.utils.unregister_class(cls)
def register():
register_classes(classes)
bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_post.append(handler.redo_post)
@@ -285,9 +256,6 @@ def register():
for mod in modules.values():
mod.register()
# Delay registering classes that depend on module classes
register_classes(late_classes)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
@@ -319,7 +287,11 @@ def unregister():
bpy.utils.previews.remove(icons)
unregister_classes(classes)
for cls in reversed(classes):
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
@@ -334,9 +306,6 @@ def unregister():
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
bpy.types.STATUSBAR_HT_header.remove(ui.draw_statusbar)
# Unregister late classes before modules
unregister_classes(late_classes)
for mod in reversed(list(modules.values())):
mod.unregister()
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 69 KiB

@@ -7,6 +7,12 @@
1.0,
1.0
],
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
@@ -48,6 +54,11 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -242,6 +253,12 @@
1.0,
1.0
],
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
@@ -283,6 +300,11 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -355,7 +377,7 @@
1.0,
1.0
],
"scene.display.shading.studio_light": "forest.exr",
"scene.display.shading.studio_light": "Default",
"scene.display.shading.studiolight_background_alpha": 0.0,
"scene.display.shading.studiolight_background_blur": 0.0,
"scene.display.shading.studiolight_intensity": 0.0,
@@ -481,12 +503,18 @@
"scene.eevee.shadow_ray_count": 1,
"scene.eevee.shadow_step_count": 6,
"scene.eevee.shadow_resolution_scale": 1.0,
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
"scene.render.border_min_y": 0.0,
"scene.render.dither_intensity": 1.0,
"scene.render.engine": "BLENDER_EEVEE",
"scene.render.engine": "BLENDER_EEVEE_NEXT",
"scene.render.film_transparent": false,
"scene.render.filter_size": 1.5,
"scene.render.fps": 24,
@@ -522,6 +550,11 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -594,7 +627,7 @@
0.800000011920929,
0.800000011920929
],
"scene.display.shading.studio_light": "forest.exr",
"scene.display.shading.studio_light": "Default",
"scene.display.shading.studiolight_background_alpha": 0.0,
"scene.display.shading.studiolight_background_blur": 0.0,
"scene.display.shading.studiolight_intensity": 0.0,
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 -12
View File
@@ -25,7 +25,6 @@ import zipfile
import tempfile
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import bonsai.tool as tool
@@ -37,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:
@@ -106,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.
-805
View File
@@ -1,805 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
"""
Shared gizmo components for reuse across modules.
"""
import bpy
import math
from typing import Optional
from mathutils import Vector
from mathutils.bvhtree import BVHTree
from mathutils.geometry import intersect_line_line
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
import gpu
from gpu_extras.batch import batch_for_shader
SNAP_CIRCLE_SEGMENTS = 16
SNAP_CIRCLE_RADIUS = 0.1
SNAP_CIRCLE_COLOR = (1.0, 0.5, 0.0, 1.0)
SNAP_LINE_WIDTH = 3.0
SNAP_MAX_RADIUS = 10.0
ARROW_SHAFT_LENGTH = 0.8
ARROW_HEAD_LENGTH = 0.2
ARROW_WIDTH = 0.015
ARROW_HEAD_WIDTH_MULTIPLIER = 10
ARROW_CIRCLE_SEGMENTS = 8
CONE_LENGTH = 1.0
CONE_RADIUS = 0.35
CONE_SEGMENTS = 16
ARC_SEGMENTS = 24
ARC_LINE_WIDTH = 0.015
PRECISION_MODE_MULTIPLIER = 0.1
class SnapManager:
"""Manages snap point visualization and mesh snapping."""
def __init__(self):
self._snap_point: Optional[tuple[float, float, float]] = None
self._draw_handler = None
self._shader = None
def set_snap_point(self, point: Optional[tuple[float, float, float]]) -> None:
"""Set snap point and register draw handler if needed."""
self._snap_point = point
if self._draw_handler is None and point is not None:
self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(self._draw, (), "WINDOW", "POST_VIEW")
self._redraw_viewport()
def clear(self) -> None:
"""Clear snap point and unregister handler."""
self._snap_point = None
if self._draw_handler is not None:
bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, "WINDOW")
self._draw_handler = None
self._redraw_viewport()
def _draw(self) -> None:
"""Draw snap point circles."""
if self._snap_point is None:
return
if self._shader is None:
self._shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self._shader.bind()
self._shader.uniform_float("color", SNAP_CIRCLE_COLOR)
gpu.state.line_width_set(SNAP_LINE_WIDTH)
for plane in ("XY", "XZ", "YZ"):
vertices = generate_circle_vertices(self._snap_point, SNAP_CIRCLE_RADIUS, SNAP_CIRCLE_SEGMENTS, plane)
batch = batch_for_shader(self._shader, "LINE_STRIP", {"pos": vertices})
batch.draw(self._shader)
gpu.state.line_width_set(1.0)
@staticmethod
def _redraw_viewport() -> None:
"""Force 3D viewport redraw."""
for area in bpy.context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
@staticmethod
def snap_to_mesh(
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
) -> Vector:
"""Snap a location to the nearest mesh element if snapping is enabled.
Args:
location: The world space location to snap
context: The Blender context
axis_vector: The axis direction of the gizmo
active_obj: The active object to exclude from snapping
Returns:
The snapped location or original location if snapping is disabled
"""
tool_settings = context.scene.tool_settings
if not tool_settings.use_snap:
return location
snap_elements = tool_settings.snap_elements_base
mesh_objects = [
obj for obj in context.visible_objects if obj.type == "MESH" and obj != active_obj and obj.visible_get()
]
if not mesh_objects:
return location
nearby_objects = []
for obj in mesh_objects:
bbox_corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
for corner in bbox_corners:
if (corner - location).length <= SNAP_MAX_RADIUS:
nearby_objects.append(obj)
break
if not nearby_objects:
return location
closest_point = None
closest_distance = float("inf")
for obj in nearby_objects:
if not obj.data.vertices:
continue
depsgraph = context.evaluated_depsgraph_get()
obj_eval = obj.evaluated_get(depsgraph)
mesh = obj_eval.to_mesh()
if not mesh.vertices:
obj_eval.to_mesh_clear()
continue
world_vertices = [obj.matrix_world @ v.co for v in mesh.vertices]
if "VERTEX" in snap_elements:
for v_co in world_vertices:
dist = (v_co - location).length
if dist < closest_distance:
closest_distance = dist
closest_point = v_co
if "EDGE" in snap_elements:
for edge in mesh.edges:
v1 = world_vertices[edge.vertices[0]]
v2 = world_vertices[edge.vertices[1]]
edge_vec = v2 - v1
edge_len_sq = edge_vec.length_squared
if edge_len_sq > 0:
t = max(0, min(1, (location - v1).dot(edge_vec) / edge_len_sq))
closest_on_edge = v1 + t * edge_vec
dist = (closest_on_edge - location).length
if dist < closest_distance:
closest_distance = dist
closest_point = closest_on_edge
if "FACE" in snap_elements:
bvh = BVHTree.FromPolygons(world_vertices, [p.vertices for p in mesh.polygons])
nearest_loc, normal, index, dist = bvh.find_nearest(location)
if nearest_loc and dist < closest_distance:
closest_distance = dist
closest_point = nearest_loc
obj_eval.to_mesh_clear()
if closest_point and closest_distance < SNAP_MAX_RADIUS:
return closest_point
return location
_snap_manager = SnapManager()
def set_snap_point(point: Optional[tuple[float, float, float]]) -> None:
_snap_manager.set_snap_point(point)
def clear_snap_point() -> None:
_snap_manager.clear()
def snap_to_mesh(
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
) -> Vector:
return _snap_manager.snap_to_mesh(location, context, axis_vector, active_obj)
def generate_circle_vertices(
center: tuple[float, float, float], radius: float, segments: int, plane: str = "XY"
) -> list[tuple[float, float, float]]:
"""Generate circle vertices in specified plane.
Args:
center: (x, y, z) tuple for circle center
radius: Circle radius
segments: Number of segments
plane: 'XY', 'XZ', or 'YZ'
Returns:
List of (x, y, z) vertices
"""
vertices = []
for i in range(segments + 1):
angle = (2 * math.pi * i) / segments
cos_a = radius * math.cos(angle)
sin_a = radius * math.sin(angle)
if plane == "XY":
vertices.append((center[0] + cos_a, center[1] + sin_a, center[2]))
elif plane == "XZ":
vertices.append((center[0] + cos_a, center[1], center[2] + sin_a))
else:
vertices.append((center[0], center[1] + cos_a, center[2] + sin_a))
return vertices
def create_quarter_circle_arc(
radius: float = 1.0, segments: int = ARC_SEGMENTS, direction: str = "LEFT", line_width: float = ARC_LINE_WIDTH
) -> tuple[tuple[float, float, float], ...]:
"""Create a quarter circle arc with cross-section thickness for visibility from all angles.
Args:
radius: Radius of the arc
segments: Number of segments for smoothness
direction: 'LEFT' for counterclockwise (0 to 90°), 'RIGHT' for clockwise (0 to -90°)
line_width: Width of the arc line in both perpendicular directions
Returns:
Tuple of arc triangles for drawing geometry visible from all angles
"""
half_width = line_width / 2
arc_points = []
if direction == "LEFT":
for i in range(segments + 1):
angle = (math.pi / 2) * (i / segments)
x = radius * math.cos(angle)
y = radius * math.sin(angle)
arc_points.append((x, y))
else:
for i in range(segments + 1):
angle = (math.pi / 2) * (i / segments)
x = -radius * math.cos(angle)
y = radius * math.sin(angle)
arc_points.append((x, y))
arc_triangles = []
for i in range(len(arc_points) - 1):
x1, y1 = arc_points[i]
x2, y2 = arc_points[i + 1]
dx, dy = x2 - x1, y2 - y1
length = (dx**2 + dy**2) ** 0.5
if length > 0:
px, py = -dy / length * half_width, dx / length * half_width
arc_triangles.extend(
[
(x1 + px, y1 + py, 0.0),
(x1 - px, y1 - py, 0.0),
(x2 + px, y2 + py, 0.0),
]
)
arc_triangles.extend(
[
(x2 + px, y2 + py, 0.0),
(x1 - px, y1 - py, 0.0),
(x2 - px, y2 - py, 0.0),
]
)
arc_triangles.extend(
[
(x1, y1, -half_width),
(x2, y2, -half_width),
(x1, y1, +half_width),
]
)
arc_triangles.extend(
[
(x1, y1, +half_width),
(x2, y2, -half_width),
(x2, y2, +half_width),
]
)
return tuple(arc_triangles)
class GizmoMovable(bpy.types.Gizmo):
"""Base class for movable gizmos with snapping and precision controls.
This class provides common functionality for gizmos that can be dragged
to modify values, including:
- Snap to mesh support (toggle with Ctrl)
- Precision mode (hold Shift for 10x slower movement)
- Header text with value and modifier hints
- Consistent invoke/exit/modal behavior
"""
__slots__ = (
"custom_shape",
"init_value",
"move_get_cb",
"move_set_cb",
"axis",
"local_axis",
"start_location",
"active_obj",
"initial_snap_state",
)
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set:
self.init_value = self.move_get_cb() if self.move_get_cb else 0.0
self.start_location = self.matrix_basis.translation.copy()
self.active_obj = context.active_object
self.initial_snap_state = context.scene.tool_settings.use_snap
return {"RUNNING_MODAL"}
def exit(self, context: bpy.types.Context, cancel: bool) -> None:
context.area.header_text_set(None)
if cancel and self.move_set_cb:
self.move_set_cb(self.init_value)
if hasattr(self, "initial_snap_state"):
context.scene.tool_settings.use_snap = self.initial_snap_state
clear_snap_point()
def get_axis_direction(self) -> Vector:
return self.axis
def modal(self, context: bpy.types.Context, event: bpy.types.Event, tweak) -> set:
region = context.region
rv3d = context.region_data
tool_settings = context.scene.tool_settings
tool_settings.use_snap = not self.initial_snap_state if event.ctrl else self.initial_snap_state
current_coord = (event.mouse_region_x, event.mouse_region_y)
view_origin = region_2d_to_origin_3d(region, rv3d, current_coord)
view_direction = region_2d_to_vector_3d(region, rv3d, current_coord)
axis_direction = self.get_axis_direction()
result = intersect_line_line(
view_origin,
view_origin + view_direction * 1000,
self.start_location,
self.start_location + axis_direction * 1000,
)
current_3d = result[1] if result else self.start_location
delta = (current_3d - self.start_location).dot(axis_direction)
if tool_settings.use_snap and self.active_obj:
snapped_pos = snap_to_mesh(current_3d, context, axis_direction, self.active_obj)
if snapped_pos != current_3d:
delta = (snapped_pos - self.start_location).dot(axis_direction)
set_snap_point(snapped_pos)
else:
clear_snap_point()
else:
clear_snap_point()
if event.shift:
delta *= PRECISION_MODE_MULTIPLIER
if self.move_set_cb:
self.move_set_cb(self.init_value + delta)
self._update_header(context, self.init_value + delta, tool_settings.use_snap, event.shift)
return {"RUNNING_MODAL"}
def _update_header(self, context: bpy.types.Context, value: float, is_snapping: bool, is_precision: bool) -> None:
header_text = f"Value: {value:.3f}m"
hints = []
if is_snapping:
hints.append("Snapping: ON")
if is_precision:
hints.append("Precision Mode (0.1x)")
hints.extend(["Ctrl: Toggle Snap", "Shift: Precision"])
if hints:
header_text += " | " + " | ".join(hints)
context.area.header_text_set(header_text)
class GizmoLock(bpy.types.Gizmo):
"""Reusable lock icon gizmo that switches between closed and open states."""
bl_idname = "VIEW3D_GT_lock"
__slots__ = (
"custom_shape_closed",
"custom_shape_open",
"prop_path",
)
tris_closed = (
(0.16803650558, 0.18791499734, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.20165449381, 0.13603900373, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(-0.11167433113, -0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.39353451133, 0.30746498704, 0.0),
(-0.44701099396, 0.18791499734, 0.0),
(-0.44701099396, -0.04477182776, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.44701099396, -0.04477182776, 0.0),
(-0.39353451133, -0.04477182776, 0.0),
)
tris_open = (
(0.16803650558, 0.18791499734, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.20165449381, 0.13603900373, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(-0.11167433113, -0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.16803650558, 0.70610702038, 0.0),
(0.16803650558, 0.70610702038, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.58656096458, 0.0),
(-0.11167433113, 0.44701099396, 0.0),
(-0.11167433113, 0.73432201147, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.11167433113, 0.73432201147, 0.0),
(-0.07805634290, 0.73432201147, 0.0),
)
def get_custom_shape(self, context: bpy.types.Context):
obj = context.active_object
if not obj:
return self.custom_shape_closed
try:
is_open = obj.path_resolve(self.prop_path)
return self.custom_shape_open if is_open else self.custom_shape_closed
except:
return self.custom_shape_closed
def setup(self) -> None:
self.custom_shape_closed = self.new_custom_shape("TRIS", self.tris_closed)
self.custom_shape_open = self.new_custom_shape("TRIS", self.tris_open)
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.get_custom_shape(context))
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.get_custom_shape(context), select_id=select_id)
class GizmoArc(bpy.types.Gizmo):
"""Reusable quarter circle arc gizmo."""
bl_idname = "VIEW3D_GT_arc"
__slots__ = (
"custom_shape_left",
"custom_shape_right",
"prop_path",
)
def setup(self) -> None:
"""Create quarter circle shapes for both LEFT and RIGHT directions."""
arc_left = create_quarter_circle_arc(radius=1.0, direction="LEFT")
arc_right = create_quarter_circle_arc(radius=1.0, direction="RIGHT")
self.custom_shape_left = self.new_custom_shape(type="TRIS", verts=arc_left)
self.custom_shape_right = self.new_custom_shape(type="TRIS", verts=arc_right)
def _get_shape_for_direction(self, context: bpy.types.Context):
obj = context.active_object
if not obj:
return self.custom_shape_left
try:
direction_value = obj.path_resolve(self.prop_path)
if "RIGHT" in str(direction_value):
return self.custom_shape_right
except:
pass
return self.custom_shape_left
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self._get_shape_for_direction(context))
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self._get_shape_for_direction(context), select_id=select_id)
class GizmoPen(bpy.types.Gizmo):
"""Reusable pen/edit icon gizmo for entering edit mode."""
bl_idname = "VIEW3D_GT_pen"
__slots__ = ("custom_shape",)
tris = (
(-0.07595771551132202, -0.2948460578918457, 0.0),
(0.16886109113693237, 0.23203276097774506, 0.0),
(0.062240585684776306, 0.28157487511634827, 0.0),
(0.07201281189918518, 0.30260589718818665, 0.0),
(0.21042980253696442, 0.321493536233902, 0.0),
(0.17863331735134125, 0.25306373834609985, 0.0),
(0.062240585684776306, 0.28157487511634827, 0.0),
(-0.1825782209634781, -0.2453039139509201, 0.0),
(-0.07595771551132202, -0.2948460578918457, 0.0),
(-0.1825782209634781, -0.2453039139509201, 0.0),
(-0.19114767014980316, -0.4032624065876007, 0.0),
(-0.07595771551132202, -0.2948460578918457, 0.0),
(0.07201281189918518, 0.30260589718818665, 0.0),
(0.10380929708480835, 0.371035635471344, 0.0),
(0.21042980253696442, 0.321493536233902, 0.0),
)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
class GizmoValidate(bpy.types.Gizmo):
"""Reusable validate/checkmark icon gizmo for confirming edits."""
bl_idname = "VIEW3D_GT_validate"
__slots__ = ("custom_shape",)
tris = (
(0.36775994300842285, 0.205583393573761, 0.0),
(0.030080009251832962, -0.1881658434867859, 0.0),
(0.030080009251832962, -0.3380376696586609, 0.0),
(-0.22017201781272888, -0.16090886294841766, 0.0),
(0.030080009251832962, -0.1881658434867859, 0.0),
(-0.22017201781272888, -0.011037036776542664, 0.0),
(0.36775994300842285, 0.205583393573761, 0.0),
(0.36775994300842285, 0.355455219745636, 0.0),
(0.030080009251832962, -0.1881658434867859, 0.0),
(-0.22017201781272888, -0.16090886294841766, 0.0),
(0.030080009251832962, -0.3380376696586609, 0.0),
(0.030080009251832962, -0.1881658434867859, 0.0),
)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
class GizmoCancel(bpy.types.Gizmo):
"""Reusable cancel/X icon gizmo for canceling edits."""
bl_idname = "VIEW3D_GT_cancel"
__slots__ = ("custom_shape",)
tris = (
(0.0, 0.048707593232393265, 0.0),
(0.0, -0.048707593232393265, 0.0),
(0.048707593232393265, 0.0, 0.0),
(0.0, 0.048707593232393265, 0.0),
(-0.21918421983718872, 0.2678918242454529, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(-0.21918421983718872, 0.2678918242454529, 0.0),
(-0.2678918242454529, 0.21918421983718872, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(-0.2678918242454529, -0.21918421983718872, 0.0),
(-0.21918421983718872, -0.2678918242454529, 0.0),
(0.2678918242454529, 0.21918421983718872, 0.0),
(0.21918421983718872, 0.2678918242454529, 0.0),
(0.0, 0.048707593232393265, 0.0),
(0.21918421983718872, -0.2678918242454529, 0.0),
(0.2678918242454529, -0.21918421983718872, 0.0),
(0.048707593232393265, 0.0, 0.0),
(0.048707593232393265, 0.0, 0.0),
(0.2678918242454529, 0.21918421983718872, 0.0),
(0.0, 0.048707593232393265, 0.0),
(0.0, 0.048707593232393265, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(0.0, -0.048707593232393265, 0.0),
(-0.048707593232393265, 0.0, 0.0),
(-0.21918421983718872, -0.2678918242454529, 0.0),
(0.0, -0.048707593232393265, 0.0),
(0.0, -0.048707593232393265, 0.0),
(0.21918421983718872, -0.2678918242454529, 0.0),
(0.048707593232393265, 0.0, 0.0),
)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
class GizmoArrow(GizmoMovable):
"""Arrow gizmo for directional value editing."""
bl_idname = "BIM_GT_gizmo_arrow"
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
def _get_arrow_triangles(self) -> tuple[tuple[float, float, float], ...]:
"""Generate arrow geometry along +X axis."""
triangles = []
triangles.extend(
[
(0, -ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
(0, +ARROW_WIDTH, 0),
]
)
triangles.extend(
[
(0, +ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, +ARROW_WIDTH, 0),
]
)
triangles.extend(
[
(0, 0, -ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
(0, 0, +ARROW_WIDTH),
]
)
triangles.extend(
[
(0, 0, +ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, +ARROW_WIDTH),
]
)
head_width = ARROW_WIDTH * ARROW_HEAD_WIDTH_MULTIPLIER
triangles.extend(
[
(ARROW_SHAFT_LENGTH, -head_width, 0),
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, +head_width, 0),
]
)
triangles.extend(
[
(ARROW_SHAFT_LENGTH, 0, -head_width),
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, 0, +head_width),
]
)
for i in range(ARROW_CIRCLE_SEGMENTS):
angle1 = (2 * math.pi * i) / ARROW_CIRCLE_SEGMENTS
angle2 = (2 * math.pi * (i + 1)) / ARROW_CIRCLE_SEGMENTS
y1 = head_width * math.cos(angle1)
z1 = head_width * math.sin(angle1)
y2 = head_width * math.cos(angle2)
z2 = head_width * math.sin(angle2)
triangles.extend(
[
(ARROW_SHAFT_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, y1, z1),
(ARROW_SHAFT_LENGTH, y2, z2),
]
)
return tuple(triangles)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self._get_arrow_triangles())
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
class GizmoCone(GizmoMovable):
"""Cone gizmo for directional value editing with local axis support."""
bl_idname = "BIM_GT_gizmo_cone"
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
def get_axis_direction(self) -> Vector:
if hasattr(self, "local_axis") and self.active_obj:
obj_rotation = self.active_obj.matrix_world.to_3x3()
axis_direction = obj_rotation @ self.local_axis
axis_direction.normalize()
return axis_direction
return self.axis
def _get_cone_triangles(self) -> tuple[tuple[float, float, float], ...]:
"""Generate cone geometry along +X axis."""
triangles = []
cone_tip_x = CONE_LENGTH
for i in range(CONE_SEGMENTS):
angle1 = (2 * math.pi * i) / CONE_SEGMENTS
angle2 = (2 * math.pi * (i + 1)) / CONE_SEGMENTS
y1 = CONE_RADIUS * math.cos(angle1)
z1 = CONE_RADIUS * math.sin(angle1)
y2 = CONE_RADIUS * math.cos(angle2)
z2 = CONE_RADIUS * math.sin(angle2)
triangles.extend(
[
(cone_tip_x, 0, 0),
(0, y1, z1),
(0, y2, z2),
]
)
triangles.extend(
[
(0, 0, 0),
(0, y2, z2),
(0, y1, z1),
]
)
return tuple(triangles)
def setup(self) -> None:
self.custom_shape = self.new_custom_shape("TRIS", self._get_cone_triangles())
def draw(self, context: bpy.types.Context) -> None:
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
self.draw_custom_shape(self.custom_shape, select_id=select_id)
+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
@@ -28,7 +28,6 @@ import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.unit
@@ -284,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")
@@ -626,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)
@@ -656,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])
@@ -953,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)
@@ -1013,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)
@@ -1215,35 +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.pset.edit_pset(
self.file,
pset=self.file.by_id(pset["id"]),
properties={"Aggregate_Index": aggregate_index, "Name": name},
)
class IfcImportSettings:
"""
@@ -21,7 +21,6 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.group
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
@@ -95,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:
@@ -105,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,
@@ -124,33 +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:
# Skip spatial elements - they should not be deleted even if empty
if aggregate.is_a("IfcSpatialElement"):
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:
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"""
@@ -326,21 +291,6 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
# copy selection query to clipboard
result = ""
for aggregate in all_parts:
aggregate_class = aggregate.is_a()
aggregate_name = aggregate.Name or ""
query = f'parent = "{aggregate_name}"'
if not result:
result = query
else:
result += f" + {query}"
if result:
bpy.context.window_manager.clipboard = result
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
self.one_level_deep = False # <-- forcibly reset
return {"FINISHED"}
+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)
+12 -44
View File
@@ -28,7 +28,6 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.unit
@@ -254,17 +253,13 @@ class ProfileImportIFC(bpy.types.Operator):
class CreateAllShapes(bpy.types.Operator):
bl_idname = "bim.create_all_shapes"
bl_label = "Test All Shapes"
bl_description = (
"Look for errors in all the shapes contained in the file.\n\nSee system console for the detailed results."
)
bl_description = "Look for errors in all the shapes contained in the file"
bl_options = {"REGISTER"}
geometry_library: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name="Geometry Library",
description="Geometry library to use for testing shape creation.",
items=[(i, i, "") for i in get_args(ifcopenshell.geom.GEOMETRY_LIBRARY)],
# By default use the same library as used for importing ifc project.
default="hybrid-cgal-simple-opencascade",
default="opencascade",
)
custom_geometry_library: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
name="Custom Geometry Library",
@@ -277,23 +272,19 @@ class CreateAllShapes(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("No IFC file is loaded.")
return False
return True
return tool.Ifc.get()
def execute(self, context):
self.file = tool.Ifc.get()
geometry_library = self.custom_geometry_library or self.geometry_library
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace")
print(f"Testing geometry library '{geometry_library}'.")
total = len(elements)
settings = ifcopenshell.geom.settings()
settings.set("keep-bounding-boxes", True)
settings_2d = ifcopenshell.geom.settings()
settings_2d.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
failures: list[ifcopenshell.entity_instance] = []
failures = []
excludes = () # For the developer to debug with
for i, element in enumerate(elements, 1):
if element.GlobalId in excludes:
@@ -312,11 +303,12 @@ class CreateAllShapes(bpy.types.Operator):
failures.append(element)
print("***** FAILURE *****")
if shape:
assert isinstance(shape, W.TriangulationElement)
geom = shape.geometry
print(
f"Success {time.time() - start:.3f}s "
f"V:{(len(geom.verts)//3)} E:{(len(geom.edges)//2)} F:{(len(geom.faces)//3)}"
"Success",
time.time() - start,
len(shape.geometry.verts),
len(shape.geometry.edges),
len(shape.geometry.faces),
)
self.report({"INFO"}, f"Failed shapes: {len(failures)}, check the system console for details.")
for failure in failures:
@@ -818,12 +810,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 +818,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 +838,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
+1 -4
View File
@@ -72,10 +72,7 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.convert_to_blender")
row = layout.row(align=True)
op_props = wm.operator_properties_last(bpy.ops.bim.create_all_shapes.idname())
row.prop(op_props, "geometry_library", text="")
row.prop(op_props, "custom_geometry_library", text="", placeholder="Custom Geometry Library")
row = layout.row()
row.operator("bim.create_all_shapes")
row = layout.row()
@@ -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 = ""
camera_rotation_checksum = ""
@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]:
@@ -19,11 +19,9 @@
import gpu
import bpy
import blf
import os
import math
import bmesh
import shapely
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
@@ -42,7 +40,7 @@ from bonsai.bim.module.drawing.helper import format_distance
from timeit import default_timer as timer
from functools import cache
from typing import Optional, Union
from collections.abc import Generator, Iterator
from collections.abc import Iterator
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
@@ -173,11 +171,15 @@ class BaseDecorator:
def get_camera_width_mm(self):
# Horrific prototype code to ensure bgl draws at drawing scales
# https://blender.stackexchange.com/questions/16493/is-there-a-way-to-fit-the-viewport-to-the-current-field-of-view
def is_landscape(render):
return render.resolution_x > render.resolution_y
camera = bpy.context.scene.camera
render = bpy.context.scene.render
# Always use the camera's ortho_scale as the width in model space
camera_width_model = camera.data.ortho_scale
if is_landscape(render):
camera_width_model = camera.data.ortho_scale
else:
camera_width_model = camera.data.ortho_scale / render.resolution_y * render.resolution_x
scale = tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
camera_width_mm = scale * camera_width_model
@@ -186,26 +188,29 @@ class BaseDecorator:
def camera_zoom_to_factor(self, zoom):
return math.pow(((zoom / 50) + math.sqrt(2)) / 2, 2)
def get_splines(self, obj: bpy.types.Object) -> Generator[list[Vector]]:
def get_splines(self, obj):
"""Iterates through splines
Args:
obj: Blender object with Curve data
:param obj: Blender object with Curve data
:yield: points of each spline, world coords
Yields:
verts: points of each spline, world coords
"""
assert type(obj.data) is bpy.types.Curve
for spline in obj.data.splines:
spline_points = spline.bezier_points if spline.bezier_points else spline.points
if len(spline_points) < 2:
continue
yield [obj.matrix_world @ p.co for p in spline_points]
def get_path_geom(self, obj: bpy.types.Object, topo: bool = True):
def get_path_geom(self, obj, topo=True):
"""Parses path geometry into line segments
:param obj: Blender object with data of type Curve
:param topo: if types of vertices are needed
Args:
obj: Blender object with data of type Curve
topo: bool; if types of vertices are needed
:return: vertices: 3-tuples of coords
Returns:
vertices: 3-tuples of coords
indices: 2-tuples of each segment verices' indices
topology: types of vertices
0: internal
@@ -236,9 +241,11 @@ class BaseDecorator:
def get_mesh_geom(self, obj, check_mode=True):
"""Parses mesh geometry into line segments
:param obj: Blender object with data of type Mesh
Args:
obj: Blender object with data of type Mesh
:return: vertices: 3-tuples of coords
Returns:
vertices: 3-tuples of coords
indices: 2-tuples of each segment verices' indices
"""
if check_mode and obj.data.is_editmode:
@@ -369,9 +376,11 @@ class BaseDecorator:
):
"""Draw text label
:param pos: bottom-center
:param multiline: ``\n`` characters will be interpreted as line breaks
aligned and centered at segment middle
Args:
pos: bottom-center
multiline: \n characters will be interpreted as line breaks
aligned and centered at segment middle
NOTE: `blf.draw` seems to ignore the \n character, so we have to split the text ourselves
and use the `line_no` argument of `draw_label`
@@ -489,7 +498,7 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@cache
def format_value(self, context, value, suppress_zero_inches=False, custom_unit=None, in_unit_length=False):
def format_value(self, context, value, custom_unit=None):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
if not precision:
@@ -500,9 +509,8 @@ class BaseDecorator:
value,
precision=precision,
decimal_places=decimal_places,
suppress_zero_inches=suppress_zero_inches,
suppress_zero_inches=True,
custom_unit=custom_unit,
in_unit_length=in_unit_length,
)
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
@@ -744,12 +752,7 @@ class DimensionDecorator(BaseDecorator):
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(
context,
length,
suppress_zero_inches=dimension_data["suppress_zero_inches"],
custom_unit=dimension_data["custom_unit"],
)
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
if isinstance(self, DiameterDecorator):
text = "D" + text
text = text_prefix + text + text_suffix
@@ -1202,13 +1205,19 @@ class PlanLevelDecorator(BaseDecorator):
def get_text():
abs_z = verts[0].z
z = helper.get_relative_z(obj, element, abs_z)
z = abs_z
# Z is in meters (Blender world space), convert to project units (feet)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
z = z / unit_scale # Convert meters to feet
if element:
spatial_container = ifcopenshell.util.element.get_container(element)
rl = self.format_value(context, z, in_unit_length=True)
if spatial_container:
container_obj = tool.Ifc.get_object(spatial_container)
if container_obj:
spatial_container_z = container_obj.matrix_world.translation.z
z = abs_z - spatial_container_z
rl = self.format_value(context, z)
text = "{}{}".format("" if z < 0 else "+", rl)
return text
@@ -1281,11 +1290,7 @@ class SectionLevelDecorator(BaseDecorator):
def get_text():
z = verts[0].z
# Z is in meters (Blender world space), convert to project units (feet)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
z = z / unit_scale # Convert meters to feet
rl = self.format_value(context, z, in_unit_length=True)
rl = self.format_value(context, z)
text = "RL {}{}".format("" if z < 0 else "+", rl)
return text
@@ -1630,7 +1635,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
@@ -1645,13 +1649,6 @@ class CutDecorator:
if not context.scene.camera:
return
# Check if any viewport is in local view - skip decorations if so
for area in context.screen.areas:
if area.type == "VIEW_3D":
space = area.spaces.active
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
return # Don't draw decorations in local view
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
self.fallback_colour = (0.3, 0.3, 0.3, 1)
@@ -1740,47 +1737,6 @@ class CutDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def cache_camera_matrix(self):
obj = bpy.context.scene.camera
# Explicit `dtype` for Blender <5.0 compatibility.
DecoratorData.camera_location_checksum = repr(
np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()
)
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).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
# Handle both old float64 and new float32 checksums for version compatibility
loc_checksum_bytes = eval(DecoratorData.camera_location_checksum)
if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64)
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32)
else: # New format: 3 * 4 bytes (float32)
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32)
loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten()
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
self.cache_camera_matrix()
return True
# Handle both old float64 and new float32 checksums for version compatibility
rot_checksum_bytes = eval(DecoratorData.camera_rotation_checksum)
if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64)
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3)
else: # New format: 9 * 4 bytes (float32)
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32)
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
@@ -1788,9 +1744,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:
@@ -1987,7 +1943,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):
@@ -2002,56 +1959,17 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
prefs = tool.Blender.get_addon_preferences()
if drawing_font := prefs.doc.drawing_font:
# Try Blender addon font folder
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
if not drawing_font_path.is_file():
# 2 — Fallback search: Windows font directories
# TODO - Linux
win_font_dirs = [
Path(os.environ.get("WINDIR", "C:\\Windows")) / "Fonts",
# Add any custom enterprise paths here if needed
]
found_font = None
for font_dir in win_font_dirs:
candidate = font_dir / drawing_font
if candidate.is_file():
found_font = candidate
break
if found_font:
drawing_font_path = found_font
else:
print(f"[BIM] Font '{drawing_font}' not found in addon or Windows fonts.")
return # Bail out without assigning fonts
font_id = blf.load(str(drawing_font_path))
for decorator in self.decorators.values():
decorator.font_id = font_id
if drawing_font_path.is_file():
font_id = blf.load(drawing_font_path.__str__())
for decorator in self.decorators.values():
decorator.font_id = font_id
def __call__(self, context):
# Check if any viewport is in local view - skip decorations if so
for area in context.screen.areas:
if area.type == "VIEW_3D":
space = area.spaces.active
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
return # Don't draw decorations in local view
# disable decorations when not in camera view
if context.region_data.view_perspective != "CAMERA":
return
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)
+26 -117
View File
@@ -18,7 +18,6 @@
import bpy
import math
import ifcopenshell.util.element
import mathutils.geometry
import ifcopenshell
import ifcopenshell.util.unit
@@ -122,45 +121,6 @@ class BoundingBox:
return retVal
def find_best_precision(fractional_inches, max_precision=256, base_tolerance=0.0001):
"""
Find the simplest fraction denominator that accurately represents the value.
Uses a sliding tolerance - smaller denominators get more tolerance.
:param fractional_inches: The fractional part of inches (0.0 to 1.0)
:param max_precision: Maximum denominator to try (256 for imperial)
:param base_tolerance: Base tolerance in inches (very strict)
:return: Best denominator (power of 2: 2, 4, 8, 16, 32, 64, 128, 256)
"""
if abs(fractional_inches) < 0.00001:
return 1 # No fraction needed
# Try denominators from coarsest to finest: 2, 4, 8, 16, 32, 64, 128, 256
for exp in range(1, 9): # 2^1 to 2^8
denom = 2**exp
if denom > max_precision:
break
# Find nearest numerator for this denominator
numer = round(fractional_inches * denom)
# Skip if numerator is 0
if numer == 0:
continue
# Check if this fraction is close enough
fraction_value = numer / denom
error = abs(fraction_value - fractional_inches)
# Very strict tolerance - only simplify if it's a near-perfect match
# This keeps 3/256 as 3/256, but allows floating point errors to be cleaned up
if error < base_tolerance:
return denom
# If no simpler fraction works, use max precision
return max_precision
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
# MeasureIt-ARCH is GPL-v3
def format_distance(
@@ -226,18 +186,14 @@ def format_distance(
if unit_length == "INCHES":
toInches = 1
if unit_length == "FEET":
toInches = 12
toInches = 11.9999
else:
toInches = 39.3700787401574887
inPerFoot = 12.0
inPerFoot = 11.9999
if isArea:
toInches = 1550
inPerFoot = 144
decInches = value * toInches
decFeet = decInches / 12
inPerFoot = 143.999
if not precision:
precision = 256
elif precision == "1":
@@ -246,6 +202,8 @@ def format_distance(
precision = int(precision.split("/")[1])
base = int(precision)
decInches = value * toInches
decFeet = decInches / 12
# Separate ft and inches
# Unless Inches are the specified Length Unit or unit_fraction is False
@@ -261,33 +219,20 @@ def format_distance(
# Separate Fractional Inches
decInches = abs(decInches) # ignore the sign for inches
inches = math.floor(decInches) # remove decimal
# Clean up floating point errors
fractional_inches = decInches - inches
tolerance = 0.01 # About 1/100 of an inch
# If the fractional part is very small, treat it as zero
if fractional_inches < tolerance:
frac = 0
# If very close to the next whole inch, round up
elif fractional_inches > (1.0 - tolerance):
frac = 0
inches += 1
if inches != 0:
frac = round(base * (decInches - inches))
else:
# Calculate fraction normally
if inches != 0:
frac = round(base * fractional_inches)
else:
frac = round(base * fractional_inches)
frac = round(base * (decInches))
# Set proper numerator and denominator
if frac != base:
# Simplify using GCD
from math import gcd
divisor = gcd(int(frac), int(base))
frac = int(frac / divisor)
base = int(base / divisor)
numcycles = int(math.log2(base))
for i in range(numcycles):
if frac % 2 == 0:
frac = int(frac / 2)
base = int(base / 2)
else:
break
else:
frac = 0
inches += 1
@@ -306,46 +251,31 @@ def format_distance(
frac = None
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
tx_dist = ""
if feet:
tx_dist += str(feet) + "'"
if not feet and not add_inches:
tx_dist += str(feet) + "'"
# Add "0' - " when we have inches but no feet
# But only add " - " separator if we actually have inches to show
if not feet and add_inches:
tx_dist += "0' - "
elif feet and add_inches:
if feet and add_inches:
tx_dist += " - "
if not feet and value < 0:
tx_dist += "-"
if add_inches:
if feet == 0 and inches == 0 and not frac:
# Special case: exactly zero, show "0"
tx_dist += "0"
elif feet == 0 and inches == 0:
pass # Has fraction, will be added below
if feet == 0 and inches == 0:
pass
else:
tx_dist += str(inches)
if add_inches and frac:
if feet == 0 and inches == 0:
pass # Has fraction, will be added below
pass
else:
tx_dist += " "
if frac:
tx_dist += str(frac) + "/" + str(base)
if add_inches or frac:
# Only add inch symbol if we actually added inch content
if inches > 0 or frac > 0 or feet == 0:
tx_dist += '"'
tx_dist += '"'
if precision == "12" and unit_system == "IMPERIAL":
tx_dist = str(round(decFeet)) + "'"
if tx_dist == '"':
tx_dist = "0' - 0\""
else:
fmt = "%1.3f"
sq_feet = round(value * toInches / inPerFoot, 4)
@@ -491,8 +421,8 @@ def ortho_view_frame(
Similar to `bpy.types.Camera.view_frame`
:param camera: camera of drawing
:param margin: margins, in scene units
:arg camera: camera of drawing
:arg margin: margins, in scene units
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
props = tool.Drawing.get_camera_props(camera)
@@ -513,8 +443,8 @@ def almost_zero(v):
def clip_segment(bounds, segm):
"""Clipping line segment to bounds
:param bounds: (xmin, xmax, ymin, ymax)
:param segm: 2 vertices of the segment
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
# LiangBarsky algorithm
@@ -563,8 +493,8 @@ def clip_segment(bounds, segm):
def elevate_segment(bounds, segm):
"""Elevate line xy-perpendicular segment vertically
:param bounds: (xmin, xmax, ymin, ymax)
:param segm: 2 vertices of the segment
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
_, _, ymin, ymax, zmin, _ = bounds
@@ -609,24 +539,3 @@ def add_newline_between_words(text: str, newline_at: int) -> str:
start = space_index + 1 # Skip the space itself
return "\n".join(result)
def get_relative_z(obj: bpy.types.Object, element, abs_z: float) -> float:
"""Return relative Z of an element, accounting for its spatial container.
:param obj: The Blender object representing the element.
:param element: The IFC entity for the object.
:param abs_z: The absolute Z value in world coordinates.
:return: Relative Z value if the element is inside a spatial container,
otherwise the absolute Z.
"""
z = abs_z
if element:
spatial_container = ifcopenshell.util.element.get_container(element)
if spatial_container:
container_obj = tool.Ifc.get_object(spatial_container)
if container_obj:
spatial_container_z = container_obj.matrix_world.translation.z
z = abs_z - spatial_container_z
return z
+83 -196
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)
@@ -932,42 +934,15 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
# Add target_view and scale classes to the parent group from IFC data
if group is not None:
existing_classes = group.get("class", "").split()
# Add target_view class
if hasattr(self, "cprops") and getattr(self.cprops, "target_view", None):
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
target_view_full_class = f"target-view-{target_view_class}"
if target_view_full_class not in existing_classes:
existing_classes.append(target_view_full_class)
# Add scale class from EPset_Drawing.Scale
drawing_pset = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing")
if drawing_pset and drawing_pset.get("Scale"):
scale_value = drawing_pset["Scale"]
# Remove "1/" prefix if it exists
if isinstance(scale_value, str) and scale_value.startswith("1/"):
scale_value = scale_value[2:]
scale_class = tool.Drawing.canonicalise_class_name(str(scale_value))
scale_full_class = f"scale-{scale_class}"
if scale_full_class not in existing_classes:
existing_classes.append(scale_full_class)
group.set("class", " ".join(existing_classes))
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)
@@ -1277,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:
@@ -1291,27 +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))
)
return classes
def is_manifold(self, obj) -> bool:
@@ -1378,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 = {}
@@ -1513,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"):
@@ -1531,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:
@@ -1896,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):
@@ -2207,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))}
@@ -2255,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()
@@ -2355,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))
@@ -2369,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"}
@@ -2721,59 +2690,21 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def set_raster_style(self, context: bpy.types.Context) -> None:
scene = context.scene # Do not remove. It is used in exec later
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used in exec later
style: dict[str, Any] = json.loads(self.drawing_style.raster_style)
VIEWPORT_SHADING_TYPE = "scene.display.shading.type"
style = json.loads(self.drawing_style.raster_style)
def preprocess(path: str, value: Any) -> tuple[str, Any, bool, bool]:
warning = False
skip = False
BLENDER_5_REMOVED = (
"scene.render.bake_bias",
"scene.render.bake_margin",
"scene.render.bake_margin_type",
"scene.render.bake_samples",
"scene.render.bake_type",
"scene.render.bake_user_scale",
"scene.render.use_bake_clear",
"scene.render.use_bake_lores_mesh",
"scene.render.use_bake_multires",
"scene.render.use_bake_selected_to_active",
"scene.render.use_bake_user_scale",
)
# 25.11.07, Blender 5+
if path == "scene.render.engine" and value in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
if value == "BLENDER_EEVEE_NEXT":
print(
f"Warning: Value 'BLENDER_EEVEE_NEXT' is outdated for property '{path}' "
"since Blender 5.0 and should be replaced with 'BLENDER_EEVEE' in shading_styles.json."
)
warning = True
value = tool.Blender.get_eevee_name()
elif tool.Blender.BLENDER_5 and path in BLENDER_5_REMOVED:
# @25.05.12
if path == "scene.render.engine" and value == "BLENDER_EEVEE":
value = "BLENDER_EEVEE_NEXT"
print(
f"Warning: Property '{path}' is removed "
"since Blender 5.0 and should be also removed from shading_styles.json."
)
warning = True
skip = True
# @25.11.11
elif (
path == "scene.display.shading.studio_light"
and value == "Default"
and style[VIEWPORT_SHADING_TYPE] in ("RENDERED", "MATERIAL")
):
value = "forest.exr"
print(
f"Warning: Value 'Default' for property '{path}' and "
f"'{VIEWPORT_SHADING_TYPE}' = '{style[VIEWPORT_SHADING_TYPE]}' is outdated "
"and should be replaced with 'forest.exr' in shading_styles.json."
f"Warning: Value 'BLENDER_EEVEE' is outdated for property '{path}' "
"since Blender 4.2 and should be replaced with 'BLENDER_EEVEE_NEXT' in shading_styles.json."
)
warning = True
# @25.05.12
elif path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
if path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
value = "THEME"
print(
f"Warning: Value 'MATERIAL' is outdated for property '{path}' "
@@ -2807,16 +2738,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
return path, value, warning, skip
paths = list(style.keys())
PRIORITY_PATHS = (
# `scene.display.shading.studio_light` values depend on `.type`
# so we got to set it first.
VIEWPORT_SHADING_TYPE,
)
paths.sort(key=lambda p: p not in PRIORITY_PATHS)
for path in paths:
value = style[path]
for path, value in style.items():
path, value, warning, skip = preprocess(path, value)
self.has_warnings_during_activation |= warning
@@ -2907,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()
@@ -2980,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()
@@ -3481,15 +3393,6 @@ class LoadDrawings(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Operator is not accessible through UI if IFC project is not saved,
# but adding poll check to avoid using Drawings UI in scripts with unsaved project.
if tool.Ifc.get_path():
return True
cls.poll_message_set("IFC project is not saved.")
return False
def _execute(self, context):
core.load_drawings(tool.Drawing)
@@ -3844,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
@@ -544,10 +544,11 @@ class Scheduler:
"""
Adds text to svg.
:param p_tags: list of cell's P tags from odt file
:param box_alignment: alignment of text in box
:param wrap_text: if True, text will be wrapped to fit in cell
:param cell_width: width of cell, used for wrapping text
Args:
p_tags: list of cell's P tags from odt file
box_alignment: alignment of text in box
wrap_text: if True, text will be wrapped to fit in cell
cell_width: width of cell, used for wrapping text
"""
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
@@ -341,29 +341,17 @@ class SheetBuilder:
assert style_data is not None
text = ""
brackets_level = 0
selector_buffer = "" # Buffer to accumulate selectors across lines
for l in style_data:
if l == "{":
if brackets_level == 0:
# Get all accumulated selector text (may span multiple lines)
# Find where the last rule ended (after last }) or start of text
last_close = text.rfind("}")
if last_close == -1:
selector_text = text
text = ""
else:
selector_text = text[last_close + 1 :]
text = text[: last_close + 1]
# Process all selectors (split by comma)
cur_line = text.splitlines()[-1]
text = text[: -len(cur_line)]
css_selectors = []
for css_selector in selector_text.split(","):
css_selector = css_selector.strip()
if css_selector: # Only process non-empty selectors
css_selector = f"{css_selector}.{prefix}"
css_selectors.append(css_selector)
# making sure cases like "text, tspan" will be
# converted to "text.prefix, tspan.prefix"
for css_selector in cur_line.split(","):
css_selector = f"{css_selector.strip()}.{prefix}"
css_selectors.append(css_selector)
text += ", ".join(css_selectors) + " "
brackets_level += 1
elif l == "}":
@@ -202,15 +202,6 @@ class SvgWriter:
) -> Self:
self.precision = precision
self.decimal_places = decimal_places
# Sort annotations to ensure FILLAREA types are drawn first (at the bottom)
def sort_key(element):
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
# Return 0 for FILLAREA to draw them first, 1 for everything else
return 0 if predefined_type == "FILL_AREA" else 1
annotations = sorted(annotations, key=sort_key)
for element in annotations:
obj = tool.Ifc.get_object(element)
if (
@@ -848,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)
@@ -870,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
@@ -890,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,
@@ -1029,9 +1012,7 @@ class SvgWriter:
# TODO: allow metric to be configurable
def get_text():
abs_z = (matrix_world @ points[0].co.xyz).z
z = helper.get_relative_z(obj, element, abs_z)
z = (matrix_world @ points[0].co.xyz).z
rl = helper.format_distance(
z,
precision=self.precision,
+2 -14
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()
@@ -116,7 +115,7 @@ class BIM_PT_camera(Panel):
# See #6686.
if (
props.has_underlay
and str(render.engine) == tool.Blender.get_eevee_name()
and str(render.engine) == "BLENDER_EEVEE_NEXT"
and ((megapixels := (render.resolution_x * render.resolution_y / 10**6)) > MEGAPIXELS_WARNING_THRESHOLD)
):
box = self.layout.box()
@@ -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)
@@ -30,7 +30,6 @@ classes = (
operator.AddSweptAreaSolidItem,
operator.AssignRepresentationLayer,
operator.CopyRepresentation,
operator.DirectProfileEdit,
operator.DisableEditingRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemStyle,
operator.DisableEditingRepresentationItems,
@@ -110,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)
@@ -161,8 +159,6 @@ def register():
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
addon_keymaps.append((km, kmi))
# Deletion.
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
addon_keymaps.append((km, kmi))
@@ -181,8 +177,6 @@ def register():
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
kmi.properties.should_save = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
addon_keymaps.append((km, kmi))
+88 -476
View File
@@ -42,7 +42,6 @@ import bonsai.core.geometry
import bonsai.core.geometry as core
import bonsai.core.aggregate
import bonsai.core.nest
import bonsai.core.spatial
import bonsai.core.style
import bonsai.core.root
import bonsai.core.drawing
@@ -428,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):
@@ -464,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,
)
@@ -717,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)
@@ -810,17 +817,9 @@ class OverrideDelete(bpy.types.Operator):
@classmethod
def poll(cls, context):
# Match `object.delete` poll for consistency.
# `object.delete` poll just checks for OBJECT mode.
poll = bpy.ops.object.delete.poll()
if poll:
return True
cls.poll_message_set("Only available in OBJECT mode")
return False
return len(context.selected_objects) > 0
def execute(self, context):
if not context.selected_objects:
return {"FINISHED"}
# Deep magick from the dawn of time
if tool.Ifc.get() is None:
bpy.ops.object.delete(use_global=self.use_global, confirm=self.confirm)
@@ -832,8 +831,6 @@ class OverrideDelete(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def invoke(self, context, event):
if not context.selected_objects:
return {"FINISHED"}
assert context.window_manager
ifc_file = tool.Ifc.get()
if ifc_file is None:
@@ -869,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):
@@ -897,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)
@@ -951,53 +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
# Skip spatial elements - they should not be deleted even if empty
if aggregate.is_a("IfcSpatialElement"):
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)
@@ -1045,18 +976,9 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context) -> bool:
# Match `outliner.delete` poll for consistency.
# `outliner.delete` just checks `area.type` == `OUTLINER`.
poll = bpy.ops.outliner.delete.poll()
if poll:
return True
cls.poll_message_set("Only available from Outliner.")
return False
return len(getattr(context, "selected_ids", [])) > 0
def execute(self, context):
if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"}
# In this override, we don't check self.hierarchy. This effectively
# makes Delete and Delete Hierarchy identical. This is on purpose, since
# non-hierarchical deletion may imply a whole bunch of potentially
@@ -1075,9 +997,6 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def invoke(self, context, event):
if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"}
assert context.window_manager
ifc_file = tool.Ifc.get()
if ifc_file:
@@ -1148,18 +1067,10 @@ class OverrideDuplicateMove(bpy.types.Operator):
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@classmethod
def poll(cls, context) -> bool:
# Match `object.duplicate_move` poll for consistency.
# `object.duplicate_move` poll checks for OBJECT mode.
poll = bpy.ops.object.duplicate_move.poll()
if poll:
return True
cls.poll_message_set("Only available in OBJECT mode")
return False
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
if not context.selected_objects:
return {"FINISHED"}
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
@@ -1182,7 +1093,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
@staticmethod
def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False):
objects_to_remove: set[bpy.types.Object] = set()
objects_to_remove = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -1240,9 +1151,6 @@ class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_options = {"REGISTER", "UNDO"}
OldToNewType = dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.object_duplicate_move_linked_aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
@@ -1265,9 +1173,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
all_old_to_new = {} # Track all duplicates created
old_to_new = {}
def select_objects_and_add_data(element: ifcopenshell.entity_instance) -> None:
def select_objects_and_add_data(element):
add_linked_aggregate_group(element)
obj = tool.Ifc.get_object(element)
obj.select_set(True)
@@ -1281,6 +1189,8 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
select_objects_and_add_data(part)
else:
index = add_linked_aggregate_pset(part, index)
# index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
@@ -1293,15 +1203,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name=self.pset_name)
ifcopenshell.api.pset.edit_pset(
ifc_file,
pset=pset,
properties=properties,
)
index += 1
else:
pass
return index
def add_linked_aggregate_group(element: ifcopenshell.entity_instance) -> None:
def add_linked_aggregate_group(element):
linked_aggregate_group = None
product_groups_name = [
r.RelatingGroup.Name
@@ -1314,21 +1229,25 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
linked_aggregate_group = ifcopenshell.api.group.add_group(ifc_file, name=self.group_name)
ifcopenshell.api.group.assign_group(ifc_file, products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new: OldToNewType) -> None:
def custom_incremental_naming_for_element_assembly(old_to_new):
for new in old_to_new.values():
if new[0].is_a("IfcElementAssembly"):
group_elements: list[ifcopenshell.entity_instance] = next(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
)
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
new_obj = tool.Ifc.get_object(new[0])
new_obj.name = f"{pset['Name']}_{pset['Aggregate_Index']:02d}"
new_obj.name = pset["Name"] + "_" + str(pset["Aggregate_Index"])
def get_max_index(parts):
psets = [
ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
for p in parts
if ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
]
if psets:
index = max([i["Index"] for i in psets if i])
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
@@ -1342,28 +1261,23 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
if linked_aggregate_group:
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
group_elements: list[ifcopenshell.entity_instance] = next(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
)
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.pset.add_pset(ifc_file, product=new[0], name=self.pset_name)
if pset["Index"] == 0:
group_elements = []
if new[0].is_a("IfcElementAssembly"):
group_elements = next(
(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
and "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
),
[],
)
properties = {
"Index": pset["Index"],
"Name": pset["Name"],
"Aggregate_Index": len(group_elements) - 1 if group_elements else 0,
"Aggregate_Index": len(group_elements) - 1,
}
else:
properties = {"Index": pset["Index"]}
@@ -1383,73 +1297,43 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
location_diff = new_obj.location - base_obj_location
new_obj.location = context.scene.cursor.location + location_diff
# Process multiple selected aggregates
selected_aggregates = []
for obj in context.selected_objects:
selected_element = tool.Ifc.get_entity(obj)
if not selected_element:
continue
# Find the aggregate element
if selected_element.is_a("IfcElementAssembly"):
selected_aggregates.append(selected_element)
elif selected_element.Decomposes:
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
selected_aggregates.append(selected_element.Decomposes[0].RelatingObject)
# Remove duplicates
selected_aggregates = list(set(selected_aggregates))
if not selected_aggregates:
self.report({"INFO"}, "No Linked Aggregates selected.")
if len(context.selected_objects) != 1:
return {"FINISHED"}
# Deselect all first
selected_obj = context.selected_objects[0]
selected_element = tool.Ifc.get_entity(selected_obj)
assert selected_element
if selected_element.is_a("IfcElementAssembly"):
pass
elif selected_element.Decomposes:
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
selected_element = selected_element.Decomposes[0].RelatingObject
selected_obj = tool.Ifc.get_object(selected_element)
else:
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
return {"FINISHED"}
select_objects_and_add_data(selected_element)
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, selected_element)
bpy.ops.object.select_all(action="DESELECT")
# Process each selected aggregate
for aggregate in selected_aggregates:
aggregate_obj = tool.Ifc.get_object(aggregate)
# Select and prepare the aggregate for duplication
select_objects_and_add_data(aggregate)
# Duplicate the aggregate
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
all_old_to_new.update(old_to_new) # Collect all duplicates
# Recreate aggregate structure
tool.Root.recreate_aggregate(old_to_new)
# Copy linked aggregate data
copy_linked_aggregate_data(old_to_new)
# Apply custom naming
custom_incremental_naming_for_element_assembly(old_to_new)
# Apply 3D cursor location if requested
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, aggregate)
# Deselect for next iteration
bpy.ops.object.select_all(action="DESELECT")
# Select all newly created aggregates
for aggregate in selected_aggregates:
if aggregate in all_old_to_new:
new_aggregate = all_old_to_new[aggregate][0]
new_aggregate_obj = tool.Ifc.get_object(new_aggregate)
if new_aggregate_obj:
new_aggregate_obj.select_set(True)
# Set active object to the first new aggregate
if all_old_to_new:
first_new_aggregate = next(iter(all_old_to_new.values()))[0]
context.view_layer.objects.active = tool.Ifc.get_object(first_new_aggregate)
new_aggregate = old_to_new[selected_element][0]
tool.Blender.set_active_object(tool.Ifc.get_object(new_aggregate))
bonsai.bim.handler.refresh_ui_data()
return all_old_to_new
return old_to_new
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
@@ -1485,7 +1369,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
self.pset_name = "BBIM_Linked_Aggregate"
refresh_start_time = time()
old_to_new = {}
original_data: dict[int, dict[int, dict[str, Any]]] = {}
original_data: dict[int, dict[int, str]] = {}
def delete_objects(element: ifcopenshell.entity_instance) -> None:
"""Remove IfcElementAssembly and it's parts."""
@@ -1531,8 +1415,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
)
tool.Blender.update_viewport()
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, dict[str, Any]]]:
group: int = next(
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]:
group = next(
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
@@ -1540,22 +1424,15 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
).id()
original_data[group] = {}
pset: dict[str, Any] = ifcopenshell.util.element.get_pset(element, self.pset_name)
index: int = pset["Index"]
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:
for part in parts:
if part.is_a("IfcElementAssembly"):
# TODO: unused expression.
original_data | get_original_data(part)
else:
try:
@@ -1593,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)
@@ -2021,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,
)
@@ -2549,264 +2420,6 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
props.is_changing_mode = False
class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.direct_profile_edit"
bl_label = "IFC Direct Profile Edit"
bl_description = "Directly enter profile/axis edit mode, or exit back to object mode"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Only allow in OBJECT or EDIT modes
if context.mode not in ("OBJECT", "EDIT_MESH", "EDIT_CURVE"):
return False
# Must have an active object
if not context.active_object:
return False
# In edit mode, we're good to go (we'll exit)
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
return True
# In object mode, check if it's an editable object type
obj = context.active_object
# Only mesh and curve objects are editable
if obj.type not in ("MESH", "CURVE"):
return False
return True
def _execute(self, context):
# Ensure we're only working with the active object
# Deselect all other objects if multiple are selected
active_obj = context.active_object
if active_obj and context.mode == "OBJECT":
if len(context.selected_objects) > 1:
self.report(
{"WARNING"},
"Currently Direct Profile Edit only works on one object at a time. Operating on active object only.",
)
bpy.ops.object.select_all(action="DESELECT")
active_obj.select_set(True)
context.view_layer.objects.active = active_obj
# If we're already in edit mode, exit to object mode and SAVE changes
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
return self.handle_exit_edit_mode(context)
# We're in object mode, try to enter profile/axis edit
return self.handle_enter_edit_mode(context)
def handle_exit_edit_mode(self, context):
"""Handle exiting from edit mode and saving changes."""
obj = context.active_object
if not obj:
return {"CANCELLED"}
# Check if we're editing a representation item
if tool.Geometry.is_representation_item(obj):
return self.exit_item_edit_mode(context, obj)
# Check if we're editing an element-level profile or axis
element = tool.Ifc.get_entity(obj)
if element and tool.Geometry.has_mesh_properties(obj.data):
return self.exit_element_edit_mode(context, obj, element)
# For other edit modes, use standard operator
return bpy.ops.bim.override_mode_set_object()
def exit_item_edit_mode(self, context, obj):
"""Exit from SweptAreaSolid item editing."""
try:
item = tool.Geometry.get_active_representation(obj)
if not item or not item.is_a("IfcSweptAreaSolid"):
return bpy.ops.bim.override_mode_set_object()
# Use the standard save workflow for items
from bonsai.bim.module.model.decorator import ProfileDecorator
ProfileDecorator.uninstall()
tool.Blender.toggle_edit_mode(context)
profile = tool.Model.export_profile(obj)
if not profile:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
tool.Blender.toggle_edit_mode(context)
ProfileDecorator.install(context)
return {"CANCELLED"}
old_profile = item.SweptArea
profile.ProfileName = old_profile.ProfileName
ifc_file = tool.Ifc.get()
for inverse in ifc_file.get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
ifcopenshell.util.element.remove_deep2(ifc_file, old_profile)
props = tool.Geometry.get_geometry_props()
if props.representation_obj:
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.disable_item_mode()
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to save item changes: {str(e)}")
return {"CANCELLED"}
def exit_element_edit_mode(self, context, obj, element):
"""Exit from element-level profile or axis editing."""
try:
mesh_props = tool.Geometry.get_mesh_props(obj.data)
# Check if we're editing axis
if mesh_props.subshape_type == "AXIS":
return bpy.ops.bim.edit_extrusion_axis()
# Otherwise, we're editing a profile
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion:
return bpy.ops.bim.edit_extrusion_profile()
except Exception as e:
self.report({"ERROR"}, f"Failed to save profile/axis changes: {str(e)}")
return {"CANCELLED"}
# Fallback to standard operator
return bpy.ops.bim.override_mode_set_object()
def handle_enter_edit_mode(self, context):
"""Handle entering edit mode from object mode."""
obj = context.active_object
if not obj:
return {"CANCELLED"}
# Check if object has mesh or curve data
if not hasattr(obj, "data") or not obj.data:
self.report({"INFO"}, "Object has no editable data")
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
self.report({"INFO"}, "Active object is not an IFC element")
return {"CANCELLED"}
# Check if this is a LAYER2 element (wall, railing, etc.)
try:
material_usage = tool.Model.get_usage_type(element)
except:
material_usage = None
if material_usage == "LAYER2":
self.report({"ERROR"}, "LAYER2 elements (walls, railings, etc.) cannot use profile editing.")
return {"CANCELLED"}
# Try to get representation
try:
representation = tool.Geometry.get_active_representation(obj)
except:
representation = None
if not representation:
self.report({"INFO"}, "Object has no active representation")
return {"CANCELLED"}
# Check for PROFILE usage (beams, columns, members) - should edit axis
if material_usage == "PROFILE":
try:
return bpy.ops.bim.enable_editing_extrusion_axis()
except Exception as e:
self.report({"ERROR"}, f"Failed to enable axis editing: {str(e)}")
return {"CANCELLED"}
# Check if this is an element with an extrusion profile (LAYER3, etc)
try:
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion and not tool.Geometry.is_representation_item(obj):
return bpy.ops.bim.enable_editing_extrusion_profile()
except Exception as e:
self.report({"ERROR"}, f"Failed to enable profile editing: {str(e)}")
return {"CANCELLED"}
# Otherwise, try to enter item mode and edit SweptAreaSolid
return self.enter_item_edit_mode(context, obj)
def enter_item_edit_mode(self, context, obj):
"""Enter item mode for SweptAreaSolid editing."""
try:
props = tool.Geometry.get_geometry_props()
if not props.representation_obj:
bpy.ops.bim.import_representation_items()
props = tool.Geometry.get_geometry_props()
# Find the SweptAreaSolid item
swept_solid_obj = None
for item_obj_data in props.item_objs:
item_obj = item_obj_data.obj
if not item_obj:
continue
try:
item = tool.Geometry.get_active_representation(item_obj)
if item and item.is_a("IfcSweptAreaSolid"):
swept_solid_obj = item_obj
break
except:
continue
if not swept_solid_obj:
self.report({"INFO"}, "No editable SweptAreaSolid item found")
tool.Geometry.disable_item_mode()
return {"CANCELLED"}
# Select and activate the swept solid object
bpy.ops.object.select_all(action="DESELECT")
swept_solid_obj.select_set(True)
context.view_layer.objects.active = swept_solid_obj
# Import the profile to prepare for editing
item = tool.Geometry.get_active_representation(swept_solid_obj)
if item and item.is_a("IfcSweptAreaSolid"):
profile = item.SweptArea
res = tool.Model.import_profile(profile, obj=swept_solid_obj)
if res is None:
self.report({"ERROR"}, "Couldn't import profile for editing")
tool.Geometry.disable_item_mode()
return {"CANCELLED"}
tool.Ifc.link(item, swept_solid_obj.data)
tool.Blender.toggle_edit_mode(context)
from bonsai.bim.module.model.decorator import ProfileDecorator
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to enter item edit mode: {str(e)}")
try:
tool.Geometry.disable_item_mode()
except:
pass
return {"CANCELLED"}
return {"CANCELLED"}
class FlipObject(bpy.types.Operator):
bl_idname = "bim.flip_object"
bl_label = "Flip Object"
@@ -3356,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,
+5 -6
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:
@@ -96,12 +96,11 @@ def update_shadow_mode(self: "BIMSolarProperties", context: bpy.types.Context) -
sun_props = tool.Blender.get_sun_props()
assert sun_props
if sun_props.sun_object is None:
light = bpy.data.lights.new(name="Sun", type="SUN")
sun = bpy.data.objects.new("Sun", light)
context.scene.collection.objects.link(sun)
sun_props.sun_object = sun
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
bpy.ops.object.move_to_collection(collection_index=0)
sun_props.sun_object = bpy.context.active_object
update_sun_path(self)
context.scene.render.engine = tool.Blender.get_eevee_name()
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
assert context.scene.display
assert context.scene.display.shading
context.scene.display.shading.light = "FLAT"

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