mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-07 08:21:42 +00:00
Compare commits
257 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9d003c22f | |||
| 2e71e002fc | |||
| 40ed2fe3ab | |||
| 379fe632a3 | |||
| 2ecaee9024 | |||
| f9a3c9a994 | |||
| 77acbff50b | |||
| 3f2cad0490 | |||
| 5ec974ff94 | |||
| 2c585b649a | |||
| 36c28f3953 | |||
| d1763237d2 | |||
| e8544d78ee | |||
| e715a9467d | |||
| 1f8c12a4c8 | |||
| 89a6eac535 | |||
| d1ffb329c8 | |||
| 4396126a43 | |||
| 7448feed44 | |||
| 6bc4046b17 | |||
| 1a83e52935 | |||
| f16d42ba14 | |||
| 75135fe45d | |||
| de89ab31cf | |||
| c8a4f1bbd2 | |||
| 5be1858767 | |||
| ace1db739f | |||
| 80504310b6 | |||
| a4dae9650f | |||
| c27a995480 | |||
| 5b4c54abd2 | |||
| 57d0beedee | |||
| d0ba675b1b | |||
| f4f2fa9535 | |||
| 67707914b9 | |||
| 12766d61d4 | |||
| 34374cba41 | |||
| f8bb04eae3 | |||
| aee8cbe479 | |||
| cb36e10279 | |||
| 7ca44b5677 | |||
| a6921b83a1 | |||
| c3c94b9c59 | |||
| 576ef336fc | |||
| b1fb541912 | |||
| 6345b052da | |||
| 061554d921 | |||
| bcaa412f4b | |||
| 9f6536b263 | |||
| 226581f76e | |||
| e833d0cd13 | |||
| ed3c95d9d7 | |||
| 7a4823a1bc | |||
| 80c0932174 | |||
| 73a238e7b0 | |||
| b95900f154 | |||
| da66e3c908 | |||
| e04cc47022 | |||
| ce427698d9 | |||
| d3beae1374 | |||
| 389d481364 | |||
| 3efd88b0d6 | |||
| 0a5388f386 | |||
| 71fa002707 | |||
| dd80115aa2 | |||
| bc66710fec | |||
| 921241fbdd | |||
| 44b9dd64f6 | |||
| bfa77255a5 | |||
| b125c651a8 | |||
| eb1c81514b | |||
| 6df5632f91 | |||
| 391a412e65 | |||
| b10415b1b3 | |||
| b1c44d42e3 | |||
| 5f65e21878 | |||
| 0d3be9d101 | |||
| eea585a3cd | |||
| 1789406299 | |||
| b503061dc0 | |||
| 90ccaf4aab | |||
| 05ff47fcff | |||
| c40ecdd550 | |||
| 3a0622e9df | |||
| 0bb03750dc | |||
| 89defa9b6b | |||
| 459aad54aa | |||
| 6f3065f6ce | |||
| 69863ce4a9 | |||
| e5ae19fd34 | |||
| afe617fb93 | |||
| d922f0b9ad | |||
| f9d2d494b1 | |||
| 806aa14bd0 | |||
| 80066ba9e7 | |||
| 7cb1f07a0b | |||
| bdb52dcffd | |||
| 933ad6c801 | |||
| 1594b9505c | |||
| 325745a822 | |||
| 74f8b047fb | |||
| 0f8c6ffe9e | |||
| fa46fe0cf0 | |||
| f0a1efa1f2 | |||
| 71c1058e9e | |||
| 7575bd4dfb | |||
| d697ed3384 | |||
| aed70c8a02 | |||
| e82ca0e932 | |||
| 5910b39766 | |||
| ea9e0c9ab4 | |||
| 2e951eb952 | |||
| 2497611abd | |||
| 90bf56d9ba | |||
| e1b73ef0ef | |||
| bcb5fd7810 | |||
| 40c5145a13 | |||
| e304c3ad31 | |||
| 0494bd9677 | |||
| fc0cdfabb9 | |||
| 2dac137801 | |||
| 8ffa6fcf47 | |||
| 8824e6e2c2 | |||
| 5dbfe539ac | |||
| d1453e1139 | |||
| 5b5dcb3c6d | |||
| 4f467d199e | |||
| a6d20c0cc4 | |||
| d5d942bb2e | |||
| 9e2c7e653f | |||
| 200c0bf773 | |||
| 381b0d379a | |||
| f48b5b9d92 | |||
| 1f47aadc94 | |||
| bb3cd7e49c | |||
| edd01d44de | |||
| 52b899d070 | |||
| 008292df0f | |||
| 87bfd182d7 | |||
| 8fcc622b8a | |||
| 9415878f57 | |||
| 919ac41a74 | |||
| 80093f3777 | |||
| 28f5d6e91c | |||
| 550779deee | |||
| e2cf60f0c1 | |||
| 546a042d40 | |||
| 86b89f2153 | |||
| 34f4aa430a | |||
| 67678cb7f1 | |||
| f05b0d946d | |||
| 486651e0b7 | |||
| 5885924bb7 | |||
| ead849625d | |||
| f52aafdd6a | |||
| b0ae0a2c82 | |||
| 4d688170e0 | |||
| 322a2179e5 | |||
| 8014980b88 | |||
| 65237cab65 | |||
| 155827528a | |||
| fb15435296 | |||
| 6bba3ebeeb | |||
| 3343f3fb35 | |||
| 1b451d45cf | |||
| f5f676cf51 | |||
| 1dd477d382 | |||
| 96d05300bb | |||
| 0073a4c0a2 | |||
| d3a5071d9d | |||
| 471bd34fc6 | |||
| 44cc12763b | |||
| b68aca04ae | |||
| dd22264877 | |||
| cc6ef0268f | |||
| feecb89c8e | |||
| cb5c488cfd | |||
| b68e8dedd2 | |||
| 0975eb455e | |||
| df0e8bba8c | |||
| 71d5239823 | |||
| 92ae79f64e | |||
| 9c1f636cd4 | |||
| 23c60b38fd | |||
| 89c039252b | |||
| 60d85b4259 | |||
| e8a382b02a | |||
| 2b3ec66017 | |||
| 97dd37b345 | |||
| e4877e8246 | |||
| e216d345c1 | |||
| 14c1b70768 | |||
| e15f222a19 | |||
| 4ab2664529 | |||
| bd0cfa5366 | |||
| 2e4b0e83a2 | |||
| cf61111386 | |||
| 3aa5ddb070 | |||
| 25dacc8fed | |||
| 7cba63af9c | |||
| 3c1d6abc7e | |||
| 8da4b9f23f | |||
| 99fa77edee | |||
| 839c5ad932 | |||
| b68ce08811 | |||
| e662308665 | |||
| a2ccff67ca | |||
| 8e045d772c | |||
| 16fd614886 | |||
| f31df307c6 | |||
| 11873a7c51 | |||
| ccccbe23f4 | |||
| 2173a562be | |||
| a11f81f2ce | |||
| d3b553c4fb | |||
| 5eb11c3516 | |||
| c195799801 | |||
| ac5a44a962 | |||
| 86880e9e62 | |||
| f8ccdf1170 | |||
| e3d8473d47 | |||
| bd5d4cecbf | |||
| 2ede8a2206 | |||
| cb77f9bd26 | |||
| 0bb21fedae | |||
| 478f90c2d4 | |||
| a016f6e5c8 | |||
| 1b2fa4f994 | |||
| 2d8383efc6 | |||
| add5132dec | |||
| fe333051df | |||
| 8d1b533f91 | |||
| fb304e141a | |||
| bd094f930e | |||
| b61000d2fe | |||
| c72cadb234 | |||
| 10642177a9 | |||
| 0b8ea28463 | |||
| 9b24f54f0d | |||
| 289645dacc | |||
| c365acb517 | |||
| 785cf56621 | |||
| 60617eecce | |||
| 26c68c00b7 | |||
| ab0a602f9f | |||
| 066326ab3b | |||
| 02713dbd39 | |||
| d58dae5d2f | |||
| ca3685900f | |||
| 5e10e61aca | |||
| 92e25ce3cb | |||
| 804a670454 | |||
| efa8382fc5 | |||
| 92f9d5f506 | |||
| 8800c14105 | |||
| ec2d0bfa4a | |||
| d684d47dc8 |
@@ -14,10 +14,6 @@ jobs:
|
||||
runner: macos-14
|
||||
arch: x64
|
||||
oldarch:
|
||||
- os: macos
|
||||
runner: macos-14
|
||||
arch: arm64
|
||||
oldarch: m1
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
@@ -34,11 +30,6 @@ 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
|
||||
@@ -52,11 +43,12 @@ jobs:
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
rm -rf ./build/*/*/*/install/*gmp* ./build/*/*/*/install/*cgal*
|
||||
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: mac-${{ matrix.arch }}
|
||||
|
||||
@@ -72,12 +64,16 @@ jobs:
|
||||
# We don't use gmpcxx, but it comes preinstalled on macos runner as arm64 bottle
|
||||
# and CGAL detects it and breaks cross compilation.
|
||||
brew uninstall --ignore-dependencies gmp
|
||||
# Otherwise Python will fallback to use arm64 `pkg-config`,
|
||||
# will pick up arm64 libraries ('zstd' in particular),
|
||||
# and break the build.
|
||||
/usr/local/bin/brew install pkg-config
|
||||
# Required by Python.
|
||||
/usr/local/bin/brew install gettext openssl
|
||||
fi
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
|
||||
python3 ./nix/build-all.py -v --diskcleanup ${MAC_INTEL} \
|
||||
IFCOS_SCHEMAS=4 CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
|
||||
python3 ./nix/build-all.py -v -py-313 --diskcleanup ${MAC_INTEL} \
|
||||
| tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
|
||||
@@ -14,33 +14,67 @@ jobs:
|
||||
submodules: recursive
|
||||
path: IfcOpenShell
|
||||
|
||||
- name: Checkout Pyodide
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
repository: pyodide/pyodide
|
||||
ref: '0.28.0a3'
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ifcopenshell_build
|
||||
ref: wasm
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
path: pyodide
|
||||
|
||||
- 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 }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/meta.yaml
|
||||
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/setup.py
|
||||
echo '#!/usr/bin/bash' > script.sh
|
||||
echo 'cd pyodide' >> script.sh
|
||||
echo 'make' >> 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
|
||||
./IfcOpenShell/pyodide/build_pyodide.sh
|
||||
FILE=`echo dist/ifcopenshell-*.whl`
|
||||
NEW_FILE=`echo $FILE | sed "s/-/+${GITHUB_SHA:0:7}-/2"`
|
||||
mv $FILE $NEW_FILE
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-logs-pyodide
|
||||
path: |
|
||||
ifcopenshell_build/*/*/logs/*.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Run wheel tests
|
||||
run: |
|
||||
cp -r IfcOpenShell/pyodide/test test
|
||||
# venv set up in build_pyodide.sh.
|
||||
source .venv/bin/activate
|
||||
uv pip install pytest-pyodide
|
||||
PYODIDE_ROOT_DIST=`pyodide config get pyodide_root`/dist
|
||||
# `pytest-pyodide` requires pyodide in 'pyodide' directory in cwd, when running `pytest`.
|
||||
cp -r $PYODIDE_ROOT_DIST test/pyodide
|
||||
cp dist/ifcopenshell-*.whl test/pyodide
|
||||
cd test
|
||||
pytest --capture=no
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/pyodide/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
git config user.name "IfcOpenBot"
|
||||
git config user.email "ifcopenbot@ifcopenshell.org"
|
||||
git add */*/install/cache-*.tar.gz
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
|
||||
@@ -47,11 +47,11 @@ jobs:
|
||||
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
# Not supported on docker
|
||||
# - name: ccache
|
||||
# uses: hendrikmuhs/ccache-action@v1
|
||||
# with:
|
||||
# key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
- name: ccache
|
||||
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
|
||||
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
- name: Run Build Script
|
||||
shell: bash
|
||||
|
||||
@@ -47,11 +47,11 @@ jobs:
|
||||
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
# Not supported on docker
|
||||
# - name: ccache
|
||||
# uses: hendrikmuhs/ccache-action@v1
|
||||
# with:
|
||||
# key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
- name: ccache
|
||||
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
|
||||
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
- name: Run Build Script
|
||||
shell: bash
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -30,14 +29,6 @@ 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
|
||||
@@ -45,21 +36,21 @@ jobs:
|
||||
7z x $_.FullName
|
||||
}
|
||||
|
||||
- name: Run Build Script
|
||||
- name: ccache
|
||||
# Use fork to resolve cache misses / duplicated cache entries on Windows.
|
||||
uses: Andrej730/ccache-action@main
|
||||
with:
|
||||
key: win-${{ matrix.arch }}
|
||||
# Windows ccache needs ~1GB
|
||||
# and with default 500MB some cache gets deleted, leading to misses.
|
||||
max-size: 5000MB
|
||||
|
||||
- name: Run Build Script And Pack .zip Archives
|
||||
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
|
||||
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
|
||||
python build-all-win.py
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
@@ -81,35 +72,6 @@ jobs:
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Package .zip Archives
|
||||
run: |
|
||||
$VERSION = 'v' + ((Get-Content VERSION).Trim())
|
||||
$SHA = ${env:GITHUB_SHA}.Substring(0, 7)
|
||||
$OUTPUT_DIR = "$env:USERPROFILE\output"
|
||||
New-Item -ItemType Directory -Force -Path $OUTPUT_DIR
|
||||
|
||||
if ("${{ matrix.python }}" -eq "3.9.11") {
|
||||
# only for the first python version the executables are assembled for upload
|
||||
cd _installed-vs2022-x64/bin
|
||||
Get-ChildItem -Path . | ForEach-Object {
|
||||
echo $_
|
||||
$exe = $_.Name
|
||||
$baseName = $exe.Substring(0, $exe.Length - 4)
|
||||
$zipName = "${baseName}-$VERSION-$SHA-win64.zip"
|
||||
7z a $zipName $exe
|
||||
}
|
||||
mv *.zip $OUTPUT_DIR
|
||||
}
|
||||
|
||||
$pyVersion = "${{ matrix.python }}"
|
||||
$pyVersionMajor = ($pyVersion -split '\.')[0..1] -join ''
|
||||
cd C:\Python\${{ matrix.python }}\Lib\site-packages
|
||||
Remove-Item -Recurse -Force ifcopenshell\__pycache__ -ErrorAction SilentlyContinue
|
||||
Get-ChildItem -Path ifcopenshell -Filter "*.pyc" -Recurse | Remove-Item -Force
|
||||
$zipName = "ifcopenshell-python-$pyVersionMajor-$VERSION-$SHA-win64.zip"
|
||||
7z a $zipName ifcopenshell
|
||||
mv $zipName $OUTPUT_DIR
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
|
||||
@@ -26,6 +26,7 @@ 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
|
||||
@@ -40,15 +41,16 @@ jobs:
|
||||
- name: Black formatter
|
||||
id: black
|
||||
run: |
|
||||
uvx black --diff --check .
|
||||
black --diff --check .
|
||||
continue-on-error: true
|
||||
|
||||
- name: Ruff check
|
||||
id: ruff
|
||||
run: |
|
||||
uvx ruff check
|
||||
# It's actually Python 3.6, but ruff only supports 3.7+, but it should do.
|
||||
uvx ruff check nix/build-all.py --target-version py37
|
||||
ERROR=0
|
||||
poe ruff-main || ERROR=1
|
||||
poe ruff-old || ERROR=1
|
||||
exit $ERROR
|
||||
continue-on-error: true
|
||||
|
||||
- name: Final check
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310, py311, py312, py313]
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310, py311, py312, py313]
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
name: ci-ifctester-org
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- src/ifctester/**
|
||||
|
||||
jobs:
|
||||
publish_website:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout ifctester_org_static_html
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: IfcOpenShell/ifctester_org_static_html
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
path: ifctester_org_static_html
|
||||
- name: Build webapp
|
||||
working-directory: ./src/ifctester
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y nodejs
|
||||
git config --global user.name 'IfcOpenBot'
|
||||
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
|
||||
make webapp-build
|
||||
- name: Commit and push
|
||||
run: |
|
||||
cp -r src/ifctester/webapp/dist/* ifctester_org_static_html/
|
||||
git -C ifctester_org_static_html add .
|
||||
git -C ifctester_org_static_html commit --allow-empty -m "$(git log --oneline -1)"
|
||||
git -C ifctester_org_static_html push
|
||||
+27
-28
@@ -49,7 +49,8 @@ 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 https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
|
||||
pip install git+https://github.com/zdhoward/aud
|
||||
pip install pytest-xdist==3.8.0
|
||||
|
||||
- name: Install C++ dependencies
|
||||
run: |
|
||||
@@ -69,16 +70,19 @@ jobs:
|
||||
libhdf5-dev libcgal-dev libeigen3-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
# rocksdb on debian distros misses RTTI?
|
||||
# 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
|
||||
- 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 \
|
||||
@@ -99,9 +103,9 @@ jobs:
|
||||
echo ${{ env.pythonLocation }}
|
||||
|
||||
mkdir build && cd build
|
||||
# Ubuntu 22.04's libocct-foundation-dev package doesn't have Config.cmake, so we provide OCC paths directly.
|
||||
# In later versions of Ubuntu, this can be simplified and the OCC paths can be removed.
|
||||
cmake \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_STANDARD=17 \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
@@ -110,37 +114,32 @@ jobs:
|
||||
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
-DUSE_MMAP=On \
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DJSON_INCLUDE_DIR=/usr/include \
|
||||
-DCGAL_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_INCLUDE_DIR=/usr/include \
|
||||
-DMPFR_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
|
||||
-DEIGEN_DIR=/usr/include/eigen3 \
|
||||
-DWITH_ROCKSDB=On \
|
||||
../cmake
|
||||
sudo make -j $(nproc)
|
||||
sudo make install
|
||||
|
||||
- name: Run IfcConvert on Sample files
|
||||
run: |
|
||||
(find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
|
||||
echo $i | tee -a log; \
|
||||
timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
|
||||
echo $i $? >> statuses; \
|
||||
done) || true
|
||||
echo Failed
|
||||
grep -v 0$ statuses
|
||||
grep -v 0$ statuses | wc -l
|
||||
echo Succeeded
|
||||
grep 0$ statuses
|
||||
grep 0$ statuses | wc -l
|
||||
# - name: Run IfcConvert on Sample 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
|
||||
run: |
|
||||
IfcConvert test/input/acad2010_walls.ifc test/input/acad2010_walls.obj
|
||||
|
||||
- name: Test ifcopenshell-python
|
||||
run: |
|
||||
@@ -150,7 +149,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 || ERROR=1
|
||||
make test-parallel || ERROR=1
|
||||
cd ../bcf && make test || ERROR=1
|
||||
pip install requests
|
||||
cd ../bsdd && make test || ERROR=1
|
||||
|
||||
+4
-1
@@ -109,4 +109,7 @@ src/bonsai/bonsai/bim/schema/Brick.ttl
|
||||
bonsaiDecoratorForLoads.code-workspace
|
||||
dev_environment.bat
|
||||
|
||||
.pixi/
|
||||
.pixi/
|
||||
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||
|
||||
@@ -37,25 +37,25 @@ Contents
|
||||
|
||||
| Name | Description | License | Service |
|
||||
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
|
||||
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
|
||||
| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [](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 | [](https://pypi.org/project/ifc4d/) |
|
||||
| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
|
||||
| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
|
||||
| [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 | [](https://pypi.org/project/bcf-client/) [](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 | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](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 | [](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 | [](https://pypi.org/project/ifc4d/) |
|
||||
| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](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 |
|
||||
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
|
||||
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
|
||||
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
|
||||
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](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 | [](https://pypi.org/project/ifccsv/) |
|
||||
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||
| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
||||
| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
||||
| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||
| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||
| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||
| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||
| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
|
||||
| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [](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\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](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 | [](https://pypi.org/project/ifccsv/) |
|
||||
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](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\* | [](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\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](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 | [](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 | [](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 | [](https://pypi.org/project/ifctester/) |
|
||||
|
||||
The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
||||
|
||||
|
||||
+25
-22
@@ -27,12 +27,6 @@ endif()
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
|
||||
message(STATUS "`ccache` is found, using it as a compiler launcher.")
|
||||
endif()
|
||||
|
||||
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
|
||||
|
||||
if(POLICY CMP0141) # 3.25+
|
||||
@@ -127,8 +121,10 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
|
||||
set(BUILD_IFCGEOM ON)
|
||||
endif()
|
||||
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
|
||||
message(STATUS "`ccache` is found, using it as a compiler launcher.")
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
|
||||
if(MSVC)
|
||||
# By default Visual Studio generators will use /Zi which is not compatible
|
||||
# with ccache, so tell Visual Studio to use /Z7 instead.
|
||||
@@ -151,11 +147,6 @@ if(MSVC AND MSVC_PARALLEL_BUILD)
|
||||
add_definitions("/MP")
|
||||
endif()
|
||||
|
||||
if (MSVC AND BUILD_SHARED_LIBS)
|
||||
# @todo how do projects normally deal with this regarding classes derived from std::exception?
|
||||
add_compile_options(/wd4275)
|
||||
endif()
|
||||
|
||||
if(NO_WARN)
|
||||
if(MSVC)
|
||||
add_compile_options("/w")
|
||||
@@ -217,6 +208,8 @@ if(NOT MINIMAL_BUILD)
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
|
||||
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(PYTHON_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(PYTHON_LIBRARY)
|
||||
UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
|
||||
@@ -259,6 +252,13 @@ endmacro()
|
||||
|
||||
if(WITH_CGAL)
|
||||
if(NOT CGAL_INCLUDE_DIR)
|
||||
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
|
||||
# and sometiems it's getting in the way.
|
||||
if(NOT DEFINED Boost_USE_STATIC_LIBS)
|
||||
set(CGAL_Boost_USE_STATIC_LIBS OFF)
|
||||
else()
|
||||
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
|
||||
endif()
|
||||
find_package(CGAL REQUIRED)
|
||||
if(NOT CGAL_DIR)
|
||||
message(
|
||||
@@ -426,7 +426,7 @@ if(WASM_BUILD)
|
||||
else()
|
||||
# @todo review this, shouldn't this be all possible header-only now?
|
||||
# ... or rewritten using C++17 features?
|
||||
set(BOOST_COMPONENTS system program_options regex thread date_time)
|
||||
set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
|
||||
endif()
|
||||
|
||||
if(USE_MMAP)
|
||||
@@ -451,8 +451,11 @@ if(NOT MINIMAL_BUILD)
|
||||
clear_wasm_sysroot()
|
||||
if(IFCXML_SUPPORT)
|
||||
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
|
||||
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04?)
|
||||
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
|
||||
# CONFIG is provided using root path, so no need to clear sysroot here.
|
||||
restore_wasm_sysroot()
|
||||
find_package(LibXml2 QUIET CONFIG)
|
||||
clear_wasm_sysroot()
|
||||
|
||||
if(NOT LibXml2_FOUND)
|
||||
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
|
||||
@@ -791,7 +794,7 @@ if(HDF5_SUPPORT)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT HDF5_INCLUDE_DIR)
|
||||
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)
|
||||
@@ -801,10 +804,11 @@ if(HDF5_SUPPORT)
|
||||
# If it failed, still try to find as a module.
|
||||
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
|
||||
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
|
||||
find_package(HDF5)
|
||||
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)."
|
||||
)
|
||||
@@ -1040,11 +1044,7 @@ if(BUILD_IFCGEOM)
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema})
|
||||
endforeach()
|
||||
if(WASM_BUILD)
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
else()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
endif()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
@@ -1173,7 +1173,10 @@ if(BUILD_IFCGEOM)
|
||||
|
||||
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
|
||||
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
target_link_libraries(geometry_mapping_ifc${schema} IfcParse IfcGeom)
|
||||
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
|
||||
if (NOT BUILD_SHARED_LIBS)
|
||||
target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
|
||||
endif()
|
||||
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
|
||||
endforeach()
|
||||
|
||||
|
||||
+205
-114
@@ -31,6 +31,7 @@ Available arguments:
|
||||
``-py-313`` - build for specific Python version
|
||||
(building for all supported Python version by default).
|
||||
``-wasm`` - compile for wasm
|
||||
``-without-xxx`` - do not build dependency ``xxx`` (e.g. ``--without-swig``)
|
||||
``-mac-cross-compile-intel`` - cross compile for Intel Mac on Apple Silicon host
|
||||
``-shared`` - build shared libraries. By default will build static.
|
||||
``-diskcleanup`` - clean up build directories after finishing building dependencies
|
||||
@@ -50,6 +51,13 @@ Used environment variables:
|
||||
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (IFC2X3; IFC4; IFC4X3_ADD2) - to be supplied as `2x3;4`
|
||||
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
|
||||
(`true` by default, any other value is considered `false`)
|
||||
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
|
||||
used to deduce `PYVERSION` (e.g. '3.13.2'), `PYTHONINCLUDE`,
|
||||
`SIDE_MODULE_CFLAGS`, `SIDE_MODULE_LDFLAGS`.
|
||||
Allows to build wasm without pyodide build environment, which can be useful for debugging build issues.
|
||||
Example value: 'pyodide/cpython/installs/python-3.13.2'
|
||||
- ``WASM_TOOLCHAIN_FILE`` - path to emscripten toolchain file from pyodide ('Emscripten.cmake')
|
||||
needed only if ``WASM_PYTHON_PATH`` is provided.
|
||||
- ``ADD_COMMIT_SHA`` - if defined with any non-empty value then
|
||||
`ADD_COMMIT_SHA` and `VERSION_OVERRIDE` will be set to `ON` while configuring IfcOpenShell
|
||||
|
||||
@@ -133,7 +141,7 @@ PROJECT_NAME = "IfcOpenShell"
|
||||
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
|
||||
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
|
||||
|
||||
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6"]
|
||||
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
|
||||
JSON_VERSION = "3.11.3"
|
||||
OCE_VERSION = "0.18.3"
|
||||
OCCT_VERSION = "7.8.1"
|
||||
@@ -146,7 +154,7 @@ SWIG_VERSION = "4.1.0"
|
||||
OPENCOLLADA_VERSION = "v1.6.68"
|
||||
HDF5_VERSION = "1.13.1"
|
||||
|
||||
GMP_VERSION = "6.2.1"
|
||||
GMP_VERSION = "6.3.0"
|
||||
MPFR_VERSION = "3.1.6" # latest is 4.1.0
|
||||
CGAL_VERSION = "v5.6.3"
|
||||
USD_VERSION = "23.05"
|
||||
@@ -193,19 +201,59 @@ def cecho(message, color=NO_COLOR):
|
||||
logger.info(f"{color}{message}\033[0m")
|
||||
|
||||
|
||||
def which(cmd: str) -> Union[str, None]:
|
||||
PATH = os.getenv("PATH")
|
||||
assert PATH
|
||||
for path in PATH.split(":"):
|
||||
if os.path.exists(path) and cmd in os.listdir(path):
|
||||
return cmd
|
||||
return None
|
||||
|
||||
|
||||
# Flags.
|
||||
APPLE = platform.system() == "Darwin"
|
||||
MAC_CROSS_COMPILE_INTEL = "mac-cross-compile-intel" in flags
|
||||
assert platform.system() == "Darwin" or not MAC_CROSS_COMPILE_INTEL
|
||||
|
||||
WASM = "wasm" in flags
|
||||
"""Build WASM outside pyodide build environment."""
|
||||
WASM_CMAKE_IS_USING_INIT_VARS = False
|
||||
if WASM:
|
||||
|
||||
def get_pyodide_config_var(var_name: str) -> str:
|
||||
output = sp.check_output(["pyodide", "config", "get", var_name], encoding="utf-8").strip()
|
||||
return output
|
||||
|
||||
if "PYODIDE_ROOT" not in os.environ:
|
||||
cecho("WARNING. Couldn't find 'PYODIDE_ROOT' in environment variables.", YELLOW)
|
||||
cecho("Assuming building wasm outside pyodide build environment and resetting necessary variables.", YELLOW)
|
||||
os.environ["SIDE_MODULE_CFLAGS"] = get_pyodide_config_var("cflags")
|
||||
os.environ["SIDE_MODULE_LDFLAGS"] = get_pyodide_config_var("ldflags")
|
||||
# Override cmake toolchain for all `emcmake` calls,
|
||||
# needed for shared libraries (resulting .so wrapper)
|
||||
# and to ensure compilation is pyodide compatible (e.g. `-fwasm-exceptions` is used in compilation flags).
|
||||
os.environ["CMAKE_TOOLCHAIN_FILE"] = get_pyodide_config_var("cmake_toolchain_file")
|
||||
required_vars = (
|
||||
"SIDE_MODULE_CFLAGS",
|
||||
"SIDE_MODULE_LDFLAGS",
|
||||
"CMAKE_TOOLCHAIN_FILE",
|
||||
)
|
||||
missing_vars = [v for v in required_vars if v not in os.environ]
|
||||
assert not missing_vars, f"Some variables required for WASM compilation are missing: {', '.join(missing_vars)}"
|
||||
|
||||
def get_pyodide_build_version() -> "tuple[int, ...]":
|
||||
pyodide_build_suffix = "pyodide-build version:"
|
||||
output = sp.check_output(["pyodide", "--version"], encoding="utf-8").strip()
|
||||
assert pyodide_build_suffix in output, output
|
||||
version_line = next(l for l in output.splitlines() if l.startswith(pyodide_build_suffix))
|
||||
version = version_line.partition(":")[2].strip()
|
||||
return tuple(map(int, version.split(".")))
|
||||
|
||||
# Pyodide still in transition from `FLAGS` to `FLAGS_INIT`.
|
||||
# `FLAGS_INIT` allow us to provide flags using environment variables
|
||||
# and providing `FLAGS` directly would break pyodide toolchain.
|
||||
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (0, 30, 8)
|
||||
|
||||
# pyodide provide empty `CXXFLAGS`, leading to issues using C++ files compiled with `-fexceptions`
|
||||
# which is used by OCCT.
|
||||
# https://github.com/pyodide/pyodide-build/issues/251
|
||||
side_module_cxx_flags = os.environ.get("SIDE_MODULE_CXXFLAGS", "")
|
||||
if side_module_cxx_flags.strip():
|
||||
print("SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').")
|
||||
print("Maybe it's time to stop overriding them in the script?")
|
||||
|
||||
os.environ["SIDE_MODULE_CXXFLAGS"] = os.environ["SIDE_MODULE_CFLAGS"]
|
||||
|
||||
# Set defaults for missing empty environment variables
|
||||
|
||||
@@ -221,9 +269,11 @@ if platform.system() == "Darwin":
|
||||
|
||||
IFCOS_NUM_BUILD_PROCS = os.getenv("IFCOS_NUM_BUILD_PROCS", multiprocessing.cpu_count() + 1)
|
||||
|
||||
CMAKE_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "cmake"))
|
||||
SCRIPT_PATH = Path(__file__).parent
|
||||
REPO_PATH = SCRIPT_PATH.parent
|
||||
CMAKE_DIR = (REPO_PATH / "cmake").resolve().__str__()
|
||||
|
||||
build_dir = os.environ.get("BUILD_DIR", os.path.join(os.path.dirname(__file__), "..", "build"))
|
||||
BUILD_DIR = os.environ.get("BUILD_DIR", (REPO_PATH / "build").__str__())
|
||||
|
||||
|
||||
if WASM:
|
||||
@@ -232,7 +282,7 @@ elif MAC_CROSS_COMPILE_INTEL:
|
||||
arch = "x86_64"
|
||||
else:
|
||||
arch = platform.machine()
|
||||
DEFAULT_DEPS_DIR = Path(build_dir) / platform.system() / arch
|
||||
DEFAULT_DEPS_DIR = Path(BUILD_DIR) / platform.system() / arch
|
||||
|
||||
if TOOLSET:
|
||||
DEFAULT_DEPS_DIR = DEFAULT_DEPS_DIR / TOOLSET
|
||||
@@ -264,6 +314,7 @@ if USE_OCCT:
|
||||
cecho(" - Compiling against official Open Cascade")
|
||||
else:
|
||||
cecho(" - Compiling against Open Cascade Community Edition")
|
||||
cecho(f"* Build Directory = {BUILD_DIR}", MAGENTA)
|
||||
cecho(f"* Dependency Directory = {DEPS_DIR}", MAGENTA)
|
||||
cecho(f" - The directory where {PROJECT_NAME} dependencies are installed.")
|
||||
cecho(f"* Build Config Type = {BUILD_CFG}", MAGENTA)
|
||||
@@ -280,6 +331,11 @@ cecho(
|
||||
""" - How many compiler processes may be run in parallel.
|
||||
"""
|
||||
)
|
||||
cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA)
|
||||
cecho(
|
||||
""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
|
||||
"""
|
||||
)
|
||||
|
||||
dependency_tree: "dict[str, tuple[str, ...]]" = {
|
||||
"IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"),
|
||||
@@ -326,11 +382,13 @@ if MAC_CROSS_COMPILE_INTEL:
|
||||
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = ["architecture=x86"]
|
||||
MAC_CROSS_COMPILE_INTEL_CXX = "clang++ -arch x86_64"
|
||||
MAC_CROSS_COMPILE_INTEL_CC = "clang -arch x86_64"
|
||||
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = ["--host=x86_64-apple-darwin"]
|
||||
else:
|
||||
MAC_CROSS_COMPILE_INTEL_ARGS = []
|
||||
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = []
|
||||
MAC_CROSS_COMPILE_INTEL_CXX = ""
|
||||
MAC_CROSS_COMPILE_INTEL_CC = ""
|
||||
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
|
||||
|
||||
OFF_ON = ["OFF", "ON"]
|
||||
BUILD_STATIC = "shared" not in flags
|
||||
@@ -344,12 +402,34 @@ PIC = "-fPIC" if BUILD_STATIC else ""
|
||||
if any(f.startswith("py-") for f in flags):
|
||||
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if f"py-{''.join(pyv.split('.')[:2])}" in flags]
|
||||
|
||||
if any(f.startswith("occt-") for f in flags):
|
||||
OCCT_VERSION = next(f.split("-", 1)[1] for f in flags if f.startswith("occt-"))
|
||||
|
||||
print(OCCT_VERSION)
|
||||
|
||||
if explicit_targets:
|
||||
targets = {dep for target in explicit_targets for dep in gather_dependencies(target)}
|
||||
else:
|
||||
targets = set(dependency_tree.keys())
|
||||
|
||||
targets = set(t for t in targets if "without-%s" % t.lower() not in flags)
|
||||
if WASM:
|
||||
SKIP_TARGETS_FOR_WASM = {
|
||||
"hdf5",
|
||||
"rocksdb",
|
||||
"opencollada",
|
||||
"swig",
|
||||
"pcre",
|
||||
"pcre2",
|
||||
"IfcGeom",
|
||||
"IfcConvert",
|
||||
"IfcGeomServer",
|
||||
}
|
||||
SKIP_TARGETS_FOR_WASM = {t.lower() for t in SKIP_TARGETS_FOR_WASM}
|
||||
skip_targets = {t for t in targets if t.lower() in SKIP_TARGETS_FOR_WASM}
|
||||
if skip_targets:
|
||||
cecho(f"Skipping targets for wasm build: {', '.join(sorted(skip_targets))}", YELLOW)
|
||||
targets.difference_update(skip_targets)
|
||||
|
||||
print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t)))))
|
||||
|
||||
@@ -358,16 +438,21 @@ yacc = "yacc" # Used during swig building process, installed with `bison` on De
|
||||
missing_commands: "list[str]" = []
|
||||
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz]
|
||||
if "wasm" in flags:
|
||||
required_commands.remove(yacc) # yacc not needed for wasm builds
|
||||
# Skip swig build for WASM.
|
||||
required_commands.append("swig")
|
||||
required_commands.append("pyodide")
|
||||
required_commands.remove(yacc)
|
||||
|
||||
for cmd in required_commands:
|
||||
if which(cmd) is None:
|
||||
if shutil.which(cmd) is None:
|
||||
missing_commands.append(cmd)
|
||||
|
||||
if missing_commands:
|
||||
raise ValueError(f"Required tools not installed or not added to PATH: {', '.join(missing_commands)}")
|
||||
|
||||
MAC_INTEL_BIN_PATH = "/usr/local/bin"
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
brew = "/usr/local/bin/brew"
|
||||
brew = f"{MAC_INTEL_BIN_PATH}/brew"
|
||||
assert os.path.exists(brew), f"For intel cross compilation the brew path is expected to be '{brew}'."
|
||||
|
||||
# identifiers for the download tool (could be less memory consuming as ints, but are more verbose as strings)
|
||||
@@ -391,6 +476,13 @@ except:
|
||||
pass
|
||||
|
||||
|
||||
def restore_env(var_name: str, old_value: Union[str, None]) -> None:
|
||||
if old_value is None:
|
||||
del os.environ[var_name]
|
||||
else:
|
||||
os.environ[var_name] = old_value
|
||||
|
||||
|
||||
def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool = False) -> str:
|
||||
"""
|
||||
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
|
||||
@@ -490,16 +582,27 @@ def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None,
|
||||
if "wasm" in flags:
|
||||
wasm.append("emcmake")
|
||||
|
||||
cmake_flags: list[str] = []
|
||||
if not WASM or not WASM_CMAKE_IS_USING_INIT_VARS:
|
||||
# For WASM we provide flags using just environment variables.
|
||||
# If we provide them using cmake vars, it will override emscripten toolchain flags.
|
||||
# Unsure if we need this in general even for non-WASM builds.
|
||||
cmake_flags.extend(
|
||||
[
|
||||
f"-DCMAKE_CXX_FLAGS='{os.environ['CXXFLAGS']}'",
|
||||
f"-DCMAKE_C_FLAGS='{os.environ['CFLAGS']}'",
|
||||
]
|
||||
)
|
||||
|
||||
run(
|
||||
[
|
||||
*wasm,
|
||||
"cmake",
|
||||
P,
|
||||
*cmake_flags,
|
||||
*cmake_args,
|
||||
f"-DCMAKE_BUILD_TYPE={BUILD_CFG}",
|
||||
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
|
||||
f"-DCMAKE_CXX_FLAGS='{os.environ['CXXFLAGS']}'",
|
||||
f"-DCMAKE_C_FLAGS='{os.environ['CFLAGS']}'",
|
||||
f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}",
|
||||
],
|
||||
cwd=cwd,
|
||||
@@ -630,7 +733,7 @@ def build_dependency(
|
||||
if isinstance(patch, str):
|
||||
patch = [patch]
|
||||
for p in patch:
|
||||
patch_abs = os.path.abspath(os.path.join(os.path.dirname(__file__), p))
|
||||
patch_abs = (SCRIPT_PATH / p).absolute().__str__()
|
||||
if os.path.exists(patch_abs):
|
||||
try:
|
||||
run(["patch", "-p1", "--batch", "--forward", "-i", patch_abs], cwd=extract_dir)
|
||||
@@ -717,12 +820,13 @@ LDFLAGS = os.environ.get("LDFLAGS", "")
|
||||
|
||||
ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
|
||||
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
if "wasm" in flags:
|
||||
CFLAGS_MINIMAL = CXXFLAGS_MINIMAL = CFLAGS = CXXFLAGS = os.environ["SIDE_MODULE_CFLAGS"]
|
||||
LDFLAGS = os.environ["SIDE_MODULE_LDFLAGS"]
|
||||
# WASM `SIDE_MODULE_` are absorbed by `emcmake` automatically.
|
||||
CXXFLAGS = CXXFLAGS_MINIMAL
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
if BUILD_STATIC:
|
||||
CXXFLAGS = f"{CXXFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS = f"{CFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden {ADDITIONAL_ARGS_STR}"
|
||||
@@ -731,8 +835,6 @@ elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
LDFLAGS = f"{LDFLAGS} -Wl,--gc-sections {ADDITIONAL_ARGS_STR}"
|
||||
else:
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
if BUILD_STATIC:
|
||||
CXXFLAGS = f"{CXXFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS = f"{CFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
@@ -812,9 +914,9 @@ if "eigen" in targets:
|
||||
)
|
||||
|
||||
if "pcre" in targets:
|
||||
OLD_CC, OLD_CCXX = None, None
|
||||
OLD_CC, OLD_CXX = None, None
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
OLD_CC, OLD_CCXX = os.environ.get("CC"), os.environ.get("CXX")
|
||||
OLD_CC, OLD_CXX = os.environ.get("CC"), os.environ.get("CXX")
|
||||
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
|
||||
os.environ["CXX"] = MAC_CROSS_COMPILE_INTEL_CXX
|
||||
# Keep it autoconf as OpenCOLLADA is pretty old and might break
|
||||
@@ -827,14 +929,8 @@ if "pcre" in targets:
|
||||
download_name=f"pcre-{PCRE_VERSION}.tar.bz2",
|
||||
)
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
if OLD_CC is None:
|
||||
del os.environ["CC"]
|
||||
else:
|
||||
os.environ["CC"] = OLD_CC
|
||||
if OLD_CCXX is None:
|
||||
del os.environ["CXX"]
|
||||
else:
|
||||
os.environ["CXX"] = OLD_CCXX
|
||||
restore_env("CC", OLD_CC)
|
||||
restore_env("CXX", OLD_CXX)
|
||||
|
||||
if "pcre2" in targets:
|
||||
build_dependency(
|
||||
@@ -866,7 +962,7 @@ if "freetype" in targets:
|
||||
download_url="https://github.com/freetype/freetype",
|
||||
download_name="freetype2",
|
||||
download_tool=download_tool_git,
|
||||
revision="VER-2-11-1",
|
||||
revision="VER-2-14-0",
|
||||
)
|
||||
|
||||
if USE_OCCT and "occ" in targets:
|
||||
@@ -883,6 +979,9 @@ if USE_OCCT and "occ" in targets:
|
||||
if OCCT_VERSION == "7.8.1":
|
||||
patches.append("./patches/occt/no_ExpToCasExe_7_8_1.patch")
|
||||
|
||||
if OCCT_VERSION == "7.9.1":
|
||||
patches.append("./patches/occt/no_ExpToCasExe_7_9_1.patch")
|
||||
|
||||
if "wasm" in flags:
|
||||
patches.append("./patches/occt/no_em_js.patch")
|
||||
|
||||
@@ -1002,6 +1101,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
# On OSX a dynamic python library is built or it would not be compatible
|
||||
# with the system python because of some threading initialization
|
||||
PYTHON_CONFIGURE_ARGS: "list[str]" = []
|
||||
original_path = ""
|
||||
if platform.system() == "Darwin":
|
||||
PYTHON_CONFIGURE_ARGS = ["--enable-shared"]
|
||||
open_ssl_prefix = run([brew, "--prefix", "openssl@3"]).strip()
|
||||
@@ -1010,6 +1110,10 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
PYTHON_CONFIGURE_ARGS.append(f"--with-openssl={open_ssl_prefix}")
|
||||
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
original_path = os.environ["PATH"]
|
||||
# Need to ensure python will pick up intel's `pkg-config`,
|
||||
# otherwise it might attempt to use ARM libraries (e.g. `zstd`) and fail.
|
||||
os.environ["PATH"] = f"{MAC_INTEL_BIN_PATH}{os.pathsep}{original_path}"
|
||||
PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"])
|
||||
|
||||
for PYTHON_VERSION in PYTHON_VERSIONS:
|
||||
@@ -1031,6 +1135,9 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
if not os.path.exists(os.path.join(DEPS_DIR, "install", f"python-{PYTHON_VERSION}")):
|
||||
raise e
|
||||
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
assert original_path
|
||||
os.environ["PATH"] = original_path
|
||||
os.environ["CPPFLAGS"] = OLD_CPP_FLAGS
|
||||
os.environ["CXXFLAGS"] = OLD_CXX_FLAGS
|
||||
os.environ["CFLAGS"] = OLD_C_FLAGS
|
||||
@@ -1076,14 +1183,23 @@ if "boost" in targets:
|
||||
if "cgal" in targets:
|
||||
gmp_args: "list[str]" = []
|
||||
mpfr_args: "list[str]" = []
|
||||
if "wasm" in flags:
|
||||
gmp_args.extend(("--disable-assembly", "--host", "none", "--enable-cxx"))
|
||||
|
||||
OLD_HOST_CC = None
|
||||
if WASM:
|
||||
if APPLE:
|
||||
# Override `HOST_CC`, otherwise `emcc` will try to use it's own `clang` which can only build
|
||||
# wasm executables and build will fail.
|
||||
os.environ["HOST_CC"] = "clang"
|
||||
# Disable assembly, otherwise `emcc -c conftest.s` will crash due to assembly mismatch.
|
||||
gmp_args.extend(("--disable-assembly", "--enable-cxx"))
|
||||
mpfr_args.extend(("--host", "none"))
|
||||
|
||||
OLD_CC = None
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
OLD_CC = os.environ.get("CC")
|
||||
# Otherwise it's using arm64 `gcc` and fails to build gmp.
|
||||
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
|
||||
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
|
||||
|
||||
build_dependency(
|
||||
name=f"gmp-{GMP_VERSION}",
|
||||
@@ -1092,10 +1208,14 @@ if "cgal" in targets:
|
||||
pre_compile_subs=(
|
||||
[("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []
|
||||
),
|
||||
download_url="https://ftp.gnu.org/gnu/gmp/",
|
||||
# Sometimes ftp.gnu.org is very slow, use ftpmirror.gnu.org as a workaround.
|
||||
download_url="https://ftpmirror.gnu.org/gnu/gmp/",
|
||||
download_name=f"gmp-{GMP_VERSION}.tar.bz2",
|
||||
)
|
||||
|
||||
if WASM and APPLE:
|
||||
restore_env("HOST_CC", OLD_HOST_CC)
|
||||
|
||||
build_dependency(
|
||||
name=f"mpfr-{MPFR_VERSION}",
|
||||
mode="autoconf",
|
||||
@@ -1105,10 +1225,7 @@ if "cgal" in targets:
|
||||
)
|
||||
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
if OLD_CC is None:
|
||||
del os.environ["CC"]
|
||||
else:
|
||||
os.environ["CC"] = OLD_CC
|
||||
restore_env("CC", OLD_CC)
|
||||
|
||||
build_dependency(
|
||||
name=f"cgal-{CGAL_VERSION}",
|
||||
@@ -1239,15 +1356,19 @@ def get_cmake_args_prefix_path(additional_paths: "Sequence[str]" = ()) -> "list[
|
||||
args_prefix_path = cmake_args_prefix_path.copy()
|
||||
args_prefix_path.extend(additional_paths)
|
||||
prefix_path = ";".join(args_prefix_path)
|
||||
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
|
||||
if WASM:
|
||||
# `emcmake` is disabling search in PATH, so we provide root paths instead.
|
||||
# Provide '/' to PATH, so it will be combined with provided root paths,
|
||||
# otherwise, depending on environment, it might not search the root path itself.
|
||||
return [f"-DCMAKE_FIND_ROOT_PATH={prefix_path}", "-DCMAKE_PREFIX_PATH=//"]
|
||||
else:
|
||||
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
|
||||
|
||||
|
||||
if "wasm" in flags:
|
||||
# Boost is built by the build script so should not be found
|
||||
# inside of the sysroot set by the emscriptem toolchain
|
||||
cmake_args.append("-DWASM_BUILD=On")
|
||||
# set Eigen3 path for WASM to avoid find_package issues
|
||||
cmake_args.append(f"-DEIGEN_DIR={DEPS_DIR}/install/eigen-install-{EIGEN_VERSION}/include/eigen3")
|
||||
|
||||
schemas = os.environ.get("IFCOS_SCHEMAS")
|
||||
if schemas:
|
||||
@@ -1257,26 +1378,9 @@ if "cgal" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/cgal-{CGAL_VERSION}")
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/gmp-{GMP_VERSION}")
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/mpfr-{MPFR_VERSION}")
|
||||
if "wasm" in flags:
|
||||
cmake_args.extend(
|
||||
[
|
||||
f"-DCGAL_INCLUDE_DIR={DEPS_DIR}/install/cgal-{CGAL_VERSION}/include",
|
||||
f"-DGMP_INCLUDE_DIR={DEPS_DIR}/install/gmp-{GMP_VERSION}/include",
|
||||
f"-DGMP_LIBRARY_DIR={DEPS_DIR}/install/gmp-{GMP_VERSION}/lib",
|
||||
f"-DMPFR_INCLUDE_DIR={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/include",
|
||||
f"-DMPFR_LIBRARY_DIR={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/lib",
|
||||
]
|
||||
)
|
||||
|
||||
if "occ" in targets and USE_OCCT:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/occt-{OCCT_VERSION}")
|
||||
if "wasm" in flags:
|
||||
cmake_args.extend(
|
||||
[
|
||||
f"-DOCC_INCLUDE_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}/include/opencascade",
|
||||
f"-DOCC_LIBRARY_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}/lib",
|
||||
]
|
||||
)
|
||||
|
||||
elif "occ" in targets:
|
||||
# We don't support find_package for OCE.
|
||||
@@ -1297,13 +1401,6 @@ else:
|
||||
|
||||
if "libxml2" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}")
|
||||
if "wasm" in flags:
|
||||
cmake_args.extend(
|
||||
[
|
||||
f"-DLIBXML2_INCLUDE_DIR={DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}/include/libxml2",
|
||||
f"-DLIBXML2_LIBRARIES={DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}/lib/libxml2.{LIBRARY_EXT}",
|
||||
]
|
||||
)
|
||||
|
||||
if "hdf5" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}")
|
||||
@@ -1333,7 +1430,7 @@ if "rocksdb" in targets:
|
||||
]
|
||||
)
|
||||
|
||||
if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets):
|
||||
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
|
||||
logger.info("\rConfiguring executables...")
|
||||
|
||||
exec_args = [
|
||||
@@ -1355,11 +1452,11 @@ if "IfcOpenShell-Python" in targets:
|
||||
# On OSX the actual Python library is not linked against.
|
||||
ADDITIONAL_ARGS = ""
|
||||
if platform.system() == "Darwin":
|
||||
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
|
||||
|
||||
if "wasm" in flags:
|
||||
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
|
||||
ADDITIONAL_ARGS = "-Wl,-undefined,dynamic_lookup"
|
||||
|
||||
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
|
||||
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
|
||||
# So cache doesn't match and running build-all.py builds most of ifcopenshell libraries twice.
|
||||
os.environ["CPPFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
|
||||
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
|
||||
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
|
||||
@@ -1369,40 +1466,47 @@ if "IfcOpenShell-Python" in targets:
|
||||
os.makedirs(python_dir, exist_ok=True)
|
||||
|
||||
def compile_python_wrapper(
|
||||
python_version: str, python_library: str, python_include: str, python_executable: Union[str, None]
|
||||
python_version: str,
|
||||
python_include: Union[str, None] = None,
|
||||
python_executable: Union[str, None] = None,
|
||||
python_path: Union[Path, None] = None,
|
||||
) -> Union[str, None]:
|
||||
"""
|
||||
:return: Path to module dir if ``python_executable`` was provided, otherwise ``None``.
|
||||
"""
|
||||
assert bool(python_path) ^ bool(python_include)
|
||||
|
||||
logger.info(f"\rConfiguring python {python_version} wrapper...")
|
||||
|
||||
cache_path = os.path.join(python_dir, "CMakeCache.txt")
|
||||
if os.path.exists(cache_path):
|
||||
os.remove(cache_path)
|
||||
|
||||
os.environ["PYTHON_LIBRARY_BASENAME"] = os.path.basename(python_library)
|
||||
|
||||
swig_prefix_paths: list[str] = []
|
||||
prefix_paths: list[str] = []
|
||||
if "swig" in targets:
|
||||
swig_prefix_paths.append(f"{DEPS_DIR}/install/swig")
|
||||
prefix_paths.append(f"{DEPS_DIR}/install/swig")
|
||||
if python_path:
|
||||
# We couldn't just prefix PATH and have to provide all variables explicitly,
|
||||
# see ifcwrap/cmake for the details.
|
||||
python_executable = (Path(python_path) / "bin" / "python3").__str__()
|
||||
python_include = run(
|
||||
[
|
||||
python_executable,
|
||||
"-c",
|
||||
"import sysconfig; print(sysconfig.get_config_var('INCLUDEPY'))",
|
||||
]
|
||||
)
|
||||
|
||||
assert python_include
|
||||
run_cmake(
|
||||
"",
|
||||
cmake_args
|
||||
+ get_cmake_args_prefix_path(swig_prefix_paths)
|
||||
+ get_cmake_args_prefix_path(prefix_paths)
|
||||
+ [
|
||||
"-DPYTHON_LIBRARY=" + python_library,
|
||||
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
|
||||
# *([f"-DPYTHON_MODULE_INSTALL_DIR={os.environ['PYTHONPATH']}/ifcopenshell"] if "wasm" in flags else []),
|
||||
*(
|
||||
[
|
||||
"-DPYTHON_MODULE_INSTALL_DIR="
|
||||
+ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "package"))
|
||||
]
|
||||
if "wasm" in flags
|
||||
else []
|
||||
),
|
||||
"-DPYTHON_INCLUDE_DIR=" + python_include,
|
||||
# Needed because pyodide is expecting setup.py to be in the root.
|
||||
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
|
||||
f"-DPYTHON_INCLUDE_DIR={python_include}",
|
||||
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
|
||||
"-DUSERSPACE_PYTHON_PREFIX="
|
||||
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
|
||||
@@ -1413,7 +1517,7 @@ if "IfcOpenShell-Python" in targets:
|
||||
|
||||
logger.info(f"\rBuilding python {python_version} wrapper... ")
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper"], cwd=python_dir)
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
|
||||
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
|
||||
|
||||
if python_executable:
|
||||
@@ -1440,32 +1544,19 @@ if "IfcOpenShell-Python" in targets:
|
||||
|
||||
if "wasm" in flags:
|
||||
compile_python_wrapper(
|
||||
f"{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.{os.environ['PYMICRO']}",
|
||||
f"{os.environ['TARGETINSTALLDIR']}/lib/libpython{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.a",
|
||||
os.environ["PYTHONINCLUDE"],
|
||||
None,
|
||||
run(["pyodide", "config", "get", "python_version"]),
|
||||
run(["pyodide", "config", "get", "python_include_dir"]),
|
||||
)
|
||||
# Copy setup.py where pyodide build system expects it.
|
||||
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
|
||||
|
||||
elif USE_CURRENT_PYTHON_VERSION:
|
||||
python_info = sysconfig.get_paths()
|
||||
|
||||
py_path_components = [sysconfig.get_config_var("LIBDIR"), sysconfig.get_config_var("INSTSONAME")]
|
||||
|
||||
if sysconfig.get_config_var("multiarchsubdir"):
|
||||
py_path_components.insert(1, sysconfig.get_config_var("multiarchsubdir").replace("/", ""))
|
||||
|
||||
python_lib = os.path.join(*py_path_components)
|
||||
|
||||
compile_python_wrapper(platform.python_version(), python_lib, python_info["include"], sys.executable)
|
||||
compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable)
|
||||
else:
|
||||
for python_version in PYTHON_VERSIONS:
|
||||
python_library = run([bash, "-c", f"ls {DEPS_DIR}/install/python-{python_version}/lib/libpython*.*"])
|
||||
python_include = run([bash, "-c", f"ls -d {DEPS_DIR}/install/python-{python_version}/include/python*"])
|
||||
python_executable = os.path.join(
|
||||
DEPS_DIR, "install", f"python-{python_version}", "bin", f"python{python_version[0]}"
|
||||
)
|
||||
|
||||
module_dir = compile_python_wrapper(python_version, python_library, python_include, python_executable)
|
||||
python_path = Path(DEPS_DIR) / "install" / f"python-{python_version}"
|
||||
module_dir = compile_python_wrapper(python_version, python_path=python_path)
|
||||
assert module_dir
|
||||
# Not sure why, but added after reading this in the logs
|
||||
# cp: /Users/runner/work/IfcOpenShell/IfcOpenShell/build/Darwin/x86_64/10.15/install/ifcopenshell/python-3.9.11: No such file or directory
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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")
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
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`
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
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)
|
||||
+2
-3
@@ -3,13 +3,12 @@ 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 --without-rocksdb --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py313 IfcOpenShell-Python
|
||||
mv package/ifcopenshell .
|
||||
cp pyodide/setup.py .
|
||||
BUILD_CFG=Release python nix/build-all.py -v --wasm --py313
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
|
||||
+45
-9
@@ -1,11 +1,47 @@
|
||||
from setuptools import setup, find_packages
|
||||
# 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
|
||||
|
||||
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={'ifcopenshell': ['util/schema/*.json', 'util/schema/*.ifc'], '': ['*.so', '*.json', '*.ifc']},
|
||||
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=[])],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
"""
|
||||
)
|
||||
@@ -62,3 +62,14 @@ ignore = [
|
||||
"UP031", # Replace % with .format
|
||||
"UP032", # Replace .format with f-string
|
||||
]
|
||||
|
||||
[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"]
|
||||
|
||||
@@ -192,6 +192,8 @@ 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)
|
||||
|
||||
@@ -179,6 +179,8 @@ 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)
|
||||
|
||||
+1
-1
@@ -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/Andrej730/aud.git --wheel-dir=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel git+https://github.com/zdhoward/aud --wheel-dir=./wheels
|
||||
# IfcOpenShell dependency - support for new typing features
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download typing_extensions --dest=./wheels
|
||||
# Required by IfcCSV
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 215 B |
Binary file not shown.
|
After 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','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','Whether or not this drawing can be referenced in other drawings.',.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 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.);
|
||||
#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.);
|
||||
#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',(#2,#3,#4,#29));
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#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.);
|
||||
#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,5 +36,7 @@ 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;
|
||||
|
||||
@@ -36,6 +36,7 @@ from bonsai.bim.ifc import IfcStore
|
||||
from mathutils import Vector
|
||||
from typing import Union
|
||||
from logging import Logger
|
||||
from math import radians
|
||||
|
||||
|
||||
class IfcExporter:
|
||||
@@ -104,7 +105,16 @@ 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)
|
||||
if tool.Geometry.is_scaled(obj):
|
||||
# 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):
|
||||
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.
|
||||
|
||||
@@ -222,8 +222,6 @@ 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()
|
||||
|
||||
|
||||
@@ -625,18 +625,13 @@ class IfcImporter:
|
||||
print("Done creating geometry")
|
||||
|
||||
def create_spatial_elements(self) -> None:
|
||||
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)
|
||||
self.create_generic_elements(self.spatial_elements)
|
||||
|
||||
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], unselectable=False, is_gross=False
|
||||
) -> None:
|
||||
def create_generic_elements(self, elements: set[ifcopenshell.entity_instance], is_gross=False) -> None:
|
||||
if isinstance(self.file, ifcopenshell.sqlite):
|
||||
return self.create_generic_sqlite_elements(elements)
|
||||
|
||||
@@ -660,10 +655,6 @@ 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])
|
||||
@@ -961,6 +952,9 @@ 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)
|
||||
@@ -1018,6 +1012,7 @@ 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)
|
||||
|
||||
@@ -94,6 +94,9 @@ 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:
|
||||
@@ -101,6 +104,10 @@ 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,
|
||||
@@ -116,6 +123,29 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
|
||||
# Second pass: delete aggregates that now have no parts
|
||||
deleted_aggregates = []
|
||||
for aggregate in aggregates_to_check:
|
||||
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||
if len(related_objects) == 0:
|
||||
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||
deleted_aggregates.append(aggregate_name)
|
||||
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj:
|
||||
ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate)
|
||||
bpy.data.objects.remove(aggregate_obj, do_unlink=True)
|
||||
|
||||
# Show info message if aggregates were deleted
|
||||
if deleted_aggregates:
|
||||
if len(deleted_aggregates) == 1:
|
||||
self.report(
|
||||
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
|
||||
)
|
||||
else:
|
||||
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||
|
||||
|
||||
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
|
||||
"""Enable editing aggregation relationship"""
|
||||
|
||||
@@ -282,7 +282,11 @@ class BIM_PT_classification_references(Panel, ReferenceUI):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bool((obj := context.active_object) and tool.Ifc.get_entity(obj))
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcObjectDefinition")
|
||||
)
|
||||
|
||||
def get_object_name(self, context: bpy.types.Context) -> str:
|
||||
assert (obj := context.active_object)
|
||||
|
||||
@@ -818,7 +818,12 @@ 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 = "For materials currently only IfcMaterials are supported"
|
||||
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_options = {"REGISTER", "UNDO"}
|
||||
|
||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
@@ -826,18 +831,36 @@ 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)
|
||||
merged_data = tool.Debug.merge_identical_objects(
|
||||
object_type, by_name_or_identification_only=self.by_name_or_identification_only
|
||||
)
|
||||
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:
|
||||
@@ -846,7 +869,8 @@ 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 ""
|
||||
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}")
|
||||
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}")
|
||||
|
||||
if merged == 0:
|
||||
return
|
||||
|
||||
@@ -61,6 +61,7 @@ classes = (
|
||||
operator.EnableEditingAssignedProduct,
|
||||
operator.EnableEditingElementFilter,
|
||||
operator.EnableEditingText,
|
||||
operator.ExcludeAnnotation,
|
||||
operator.ExpandSheet,
|
||||
operator.LoadDrawings,
|
||||
operator.LoadReferences,
|
||||
|
||||
@@ -235,13 +235,18 @@ class DecoratorData:
|
||||
cut_cache = {}
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
camera_location_checksum = None
|
||||
camera_rotation_checksum = None
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
cls.cut_cache = {}
|
||||
cls.layerset_cache = {}
|
||||
cls.fill_cache = {}
|
||||
|
||||
@classmethod
|
||||
def load(cls, handler):
|
||||
cls.is_loaded = True
|
||||
cls.cut_cache = {}
|
||||
cls.layerset_cache = {}
|
||||
cls.fill_cache = {}
|
||||
|
||||
text = {}
|
||||
dimension = {}
|
||||
@@ -347,12 +352,10 @@ 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)
|
||||
@@ -364,11 +367,20 @@ class DecoratorData:
|
||||
literal_data = {
|
||||
"Literal": literal_value,
|
||||
"BoxAlignment": literal.BoxAlignment,
|
||||
"CurrentValue": tool.Drawing.replace_text_literal_variables(literal_value, product),
|
||||
"CurrentValue": tool.Drawing.replace_text_literal_variables(
|
||||
literal_value, product, reverse_list, list_separator
|
||||
),
|
||||
}
|
||||
literals_data.append(literal_data)
|
||||
|
||||
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
|
||||
return {
|
||||
"Literals": literals_data,
|
||||
"FontSize": font_size,
|
||||
"Symbol": symbol,
|
||||
"Newline_At": newline_at,
|
||||
"Reverse_List": reverse_list,
|
||||
"List_Separator": list_separator,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
||||
|
||||
@@ -22,6 +22,7 @@ import blf
|
||||
import math
|
||||
import bmesh
|
||||
import shapely
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
@@ -1625,6 +1626,7 @@ 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
|
||||
@@ -1727,6 +1729,30 @@ class CutDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def cache_camera_matrix(self):
|
||||
obj = bpy.context.scene.camera
|
||||
DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
|
||||
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
|
||||
|
||||
def is_camera_moved(self):
|
||||
if not DecoratorData.camera_location_checksum:
|
||||
self.cache_camera_matrix()
|
||||
return True # Let's be conservative
|
||||
obj = bpy.context.scene.camera
|
||||
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
|
||||
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
|
||||
rot_real = np.array(obj.matrix_world.to_3x3())
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
if angle_rad > 0.0017453292519943296: # 0.1 degrees
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
return False
|
||||
|
||||
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
has_cut_cache = element.id() in DecoratorData.cut_cache
|
||||
has_fill_cache = element.id() in DecoratorData.fill_cache
|
||||
@@ -1734,9 +1760,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 context.scene.camera.select_get():
|
||||
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_cut(context, obj, element)
|
||||
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
@@ -1933,8 +1959,7 @@ class DecorationsHandler:
|
||||
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
|
||||
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
||||
if not DecoratorData.is_loaded:
|
||||
DecoratorData.load(handler)
|
||||
DecoratorData.clear_cache()
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
@@ -1961,5 +1986,8 @@ class DecorationsHandler:
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
if not DecoratorData.is_loaded:
|
||||
DecoratorData.load(self)
|
||||
|
||||
for obj, decorator in DecoratorData.data["object_decorators"]:
|
||||
decorator.decorate(context, obj)
|
||||
|
||||
@@ -106,14 +106,17 @@ 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=ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW"),
|
||||
context=ifc_context,
|
||||
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":
|
||||
@@ -207,12 +210,6 @@ 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
|
||||
@@ -435,21 +432,19 @@ 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
|
||||
# Hie annotations.
|
||||
# Hide annotations.
|
||||
for obj in collection.objects:
|
||||
if context.view_layer.objects.get(obj.name):
|
||||
previous_visibility[obj.name] = obj.hide_get()
|
||||
@@ -626,7 +621,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
path.attrib["d"] = d
|
||||
group.append(g)
|
||||
|
||||
def generate_wall_layers(self, context: bpy.types.Context, root) -> None:
|
||||
def generate_material_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
|
||||
@@ -795,8 +790,9 @@ class CreateDrawing(bpy.types.Operator):
|
||||
edge_bm.to_mesh(edge_mesh)
|
||||
edge_bm.free()
|
||||
|
||||
actual_path = svg_path[0:-4] + "0001.svg"
|
||||
freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter")
|
||||
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)
|
||||
@@ -840,6 +836,8 @@ 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)
|
||||
|
||||
@@ -937,12 +935,14 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if self.cprops.cut_mode == "BISECT":
|
||||
self.remove_cut_linework(root)
|
||||
self.generate_bisect_linework(context, root)
|
||||
self.generate_wall_layers(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)
|
||||
elif self.cprops.cut_mode == "OPENCASCADE":
|
||||
self.move_projection_to_bottom(root)
|
||||
self.generate_wall_layers(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)
|
||||
|
||||
@@ -1252,6 +1252,8 @@ 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:
|
||||
@@ -1264,6 +1266,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
else:
|
||||
classes.append("material-null")
|
||||
|
||||
# ─── Layer ─────────────────────────────────────────────────
|
||||
if layer:
|
||||
classes.append(layer.is_a())
|
||||
layer_material = layer.Material
|
||||
@@ -1276,12 +1279,19 @@ class CreateDrawing(bpy.types.Operator):
|
||||
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
|
||||
classes.append(f"layer-material-category-{layer_material_category}")
|
||||
|
||||
# ─── Metadata ──────────────────────────────────────────────
|
||||
for key in self.metadata:
|
||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||
if value:
|
||||
classes.append(
|
||||
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
||||
)
|
||||
|
||||
# ─── Target View ───────────────────────────────────────────
|
||||
if getattr(self.cprops, "target_view", None):
|
||||
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
|
||||
classes.append(f"target-view-{target_view_class}")
|
||||
|
||||
return classes
|
||||
|
||||
def is_manifold(self, obj) -> bool:
|
||||
@@ -1348,7 +1358,13 @@ 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", "Material.Name"]
|
||||
join_criteria = [
|
||||
"class",
|
||||
"material.Name",
|
||||
"/Pset_.*Common/.Status",
|
||||
"EPset_Status.Status",
|
||||
"EPset_Status.UserDefinedStatus",
|
||||
]
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
joined_paths = {}
|
||||
@@ -1477,11 +1493,10 @@ class CreateDrawing(bpy.types.Operator):
|
||||
joined_paths.setdefault(hash_keys, []).append(el)
|
||||
|
||||
for key, els in joined_paths.items():
|
||||
polygons = []
|
||||
classes = set()
|
||||
queue = []
|
||||
|
||||
for el in els:
|
||||
classes.update(el.attrib["class"].split())
|
||||
classes = set(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"):
|
||||
@@ -1496,31 +1511,30 @@ class CreateDrawing(bpy.types.Operator):
|
||||
coords.append(coords[0])
|
||||
if len(coords) > 2 and coords[0] == coords[-1]:
|
||||
is_closed_polygon = True
|
||||
polygons.append(shapely.Polygon(coords))
|
||||
queue.append((shapely.Polygon(coords), classes))
|
||||
if is_closed_polygon:
|
||||
el.getparent().remove(el)
|
||||
|
||||
try:
|
||||
merged_polygons = shapely.ops.unary_union(polygons)
|
||||
except:
|
||||
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
|
||||
merged_polygons = polygons
|
||||
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))
|
||||
|
||||
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(classes)))
|
||||
g.set("class", " ".join(list(polygon_classes)))
|
||||
group.append(g)
|
||||
|
||||
def drawing_to_model_co(self, x: float, y: float) -> Vector:
|
||||
@@ -1862,13 +1876,15 @@ 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):
|
||||
@@ -2171,27 +2187,10 @@ 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))}
|
||||
@@ -2236,15 +2235,10 @@ class ActivateModel(bpy.types.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=model,
|
||||
should_reload=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
|
||||
# restore visibility after hide_view_clear()
|
||||
for obj, hide_status in visibility_status.items():
|
||||
obj.hide_set(hide_status)
|
||||
|
||||
tool.Blender.reset_object_visibility()
|
||||
tool.Drawing.hide_all_drawing_collections()
|
||||
tool.Blender.update_viewport()
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
@@ -2341,9 +2335,8 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
|
||||
dprops.active_drawing_id = self.drawing
|
||||
dprops.drawing_styles.clear()
|
||||
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
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))
|
||||
@@ -2356,10 +2349,22 @@ 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="")
|
||||
# See 6452 and 6478.
|
||||
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
||||
# Restore the scale after update if needed
|
||||
if is_reflected:
|
||||
camera.scale = (-1, -1, -1)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -2835,8 +2840,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not props.schedules:
|
||||
cls.poll_message_set("No schedule selected.")
|
||||
return False
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return props.schedules and props.sheets and prefs.data_dir
|
||||
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):
|
||||
props = tool.Drawing.get_document_props()
|
||||
@@ -2903,8 +2913,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not props.references:
|
||||
cls.poll_message_set("No reference selected.")
|
||||
return False
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
return props.references and props.sheets and bim_props.data_dir
|
||||
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):
|
||||
props = tool.Drawing.get_document_props()
|
||||
@@ -3753,3 +3768,19 @@ 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)
|
||||
|
||||
@@ -498,6 +498,9 @@ 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"),
|
||||
@@ -744,6 +747,11 @@ 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.",
|
||||
@@ -760,6 +768,8 @@ 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
|
||||
|
||||
@@ -796,6 +806,8 @@ 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
|
||||
|
||||
@@ -850,6 +862,19 @@ class BIMAnnotationProperties(PropertyGroup):
|
||||
)
|
||||
is_adding_type: bpy.props.BoolProperty(default=False)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
tag_rotation_mode: bpy.props.EnumProperty(
|
||||
name="Tag Rotation Mode",
|
||||
description="How to orient the tag relative to the tagged object",
|
||||
items=[
|
||||
("NONE", "No Rotation", "Keep tag in default orientation"),
|
||||
("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"),
|
||||
("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"),
|
||||
("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"),
|
||||
("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"),
|
||||
("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"),
|
||||
],
|
||||
default="NONE",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
object_type: str
|
||||
|
||||
@@ -839,6 +839,12 @@ 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)
|
||||
@@ -855,7 +861,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
|
||||
text_literal.Literal, product or element, reverse_list, list_separator
|
||||
)
|
||||
field.attrib["class"] = classes_str
|
||||
|
||||
@@ -875,7 +881,9 @@ class SvgWriter:
|
||||
line_number = 0
|
||||
|
||||
for text_literal in text_literals:
|
||||
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element)
|
||||
text = tool.Drawing.replace_text_literal_variables(
|
||||
text_literal.Literal, product or element, reverse_list, list_separator
|
||||
)
|
||||
text_tags = self.create_text_tag(
|
||||
text,
|
||||
text_position_svg,
|
||||
|
||||
@@ -99,12 +99,13 @@ 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()
|
||||
@@ -193,6 +194,7 @@ 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"
|
||||
|
||||
|
||||
@@ -590,6 +592,10 @@ 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")
|
||||
@@ -648,6 +654,12 @@ 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,9 +180,6 @@ 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:
|
||||
@@ -231,7 +228,6 @@ 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="")
|
||||
@@ -251,6 +247,9 @@ 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",
|
||||
@@ -302,6 +301,8 @@ 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,
|
||||
@@ -314,7 +315,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
),
|
||||
enable_editing=False,
|
||||
)
|
||||
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
|
||||
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]
|
||||
|
||||
def hotkey_S_A(self):
|
||||
if bpy.ops.bim.add_annotation.poll():
|
||||
@@ -344,4 +353,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
related_object = tool.Ifc.get_object(related_product)
|
||||
|
||||
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
|
||||
rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode
|
||||
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode)
|
||||
|
||||
@@ -109,8 +109,9 @@ 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":
|
||||
obj.scale = (-1, -1, -1)
|
||||
obj.rotation_euler = (0.0, 0.0, math.radians(180))
|
||||
# Only update if scale isn't already (-1, -1, -1)
|
||||
if obj.scale != (-1, -1, -1):
|
||||
obj.scale = (-1, -1, -1)
|
||||
else:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
|
||||
@@ -427,9 +427,7 @@ 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):
|
||||
@@ -465,9 +463,6 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@@ -721,9 +716,6 @@ 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)
|
||||
@@ -866,6 +858,10 @@ 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):
|
||||
@@ -890,13 +886,28 @@ 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.")
|
||||
return {"FINISHED"}
|
||||
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
|
||||
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)
|
||||
@@ -929,6 +940,49 @@ class OverrideDelete(bpy.types.Operator):
|
||||
data["old_file"].redo()
|
||||
tool.Ifc.set(data["new_file"])
|
||||
|
||||
def track_aggregates(self, objects_to_remove):
|
||||
"""Track aggregates that contain objects being deleted"""
|
||||
aggregates_to_check = set()
|
||||
for obj in objects_to_remove:
|
||||
if not tool.Blender.is_valid_data_block(obj):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
aggregates_to_check.add(aggregate)
|
||||
return aggregates_to_check
|
||||
|
||||
def delete_empty_aggregates(self, aggregates_to_check):
|
||||
"""Delete aggregates that now have no parts"""
|
||||
deleted_aggregates = []
|
||||
for aggregate in aggregates_to_check:
|
||||
# Check if aggregate still exists (might have been deleted already)
|
||||
try:
|
||||
aggregate.id()
|
||||
except:
|
||||
continue
|
||||
|
||||
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||
if len(related_objects) == 0:
|
||||
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||
deleted_aggregates.append(aggregate_name)
|
||||
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj):
|
||||
tool.Geometry.delete_ifc_object(aggregate_obj)
|
||||
|
||||
# Show info message if aggregates were deleted
|
||||
if deleted_aggregates:
|
||||
if len(deleted_aggregates) == 1:
|
||||
self.report(
|
||||
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
|
||||
)
|
||||
else:
|
||||
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||
|
||||
def process_arrays(self, context: bpy.types.Context) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
selected_objects = set(context.selected_objects)
|
||||
@@ -1904,9 +1958,6 @@ 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,
|
||||
)
|
||||
|
||||
@@ -2984,6 +3035,7 @@ 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,8 +157,6 @@ 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"]
|
||||
|
||||
@@ -41,7 +41,7 @@ class SolarData:
|
||||
|
||||
@classmethod
|
||||
def sun_position(cls):
|
||||
return tool.Blender.get_sun_position_addon()
|
||||
return tool.Blender.get_addon("sun_position")
|
||||
|
||||
@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_sun_position_addon()
|
||||
# sun_position = tool.Blender.get_addon("sun_position")
|
||||
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
|
||||
# sun_pos_props.time,
|
||||
# sun_pos_props.latitude,
|
||||
|
||||
@@ -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_sun_position_addon()
|
||||
sun_position = tool.Blender.get_addon("sun_position")
|
||||
now = datetime.datetime.now()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||
|
||||
@@ -323,20 +323,23 @@ class ObjectMaterialData:
|
||||
|
||||
@classmethod
|
||||
def total_thickness(cls):
|
||||
if cls.material:
|
||||
layers = []
|
||||
if cls.material.is_a("IfcMaterialLayerSetUsage"):
|
||||
layers = cls.material.ForLayerSet.MaterialLayers
|
||||
elif cls.material.is_a("IfcMaterialLayerSet"):
|
||||
layers = cls.material.MaterialLayers
|
||||
thickness = sum([l.LayerThickness for l in layers or []])
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
assert bpy.context.scene
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
precision = None
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = prefs.doc.imperial_precision
|
||||
return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
|
||||
if not cls.material:
|
||||
return
|
||||
layers = []
|
||||
if cls.material.is_a("IfcMaterialLayerSetUsage"):
|
||||
layers = cls.material.ForLayerSet.MaterialLayers
|
||||
elif cls.material.is_a("IfcMaterialLayerSet"):
|
||||
layers = cls.material.MaterialLayers
|
||||
if not layers:
|
||||
return
|
||||
thickness = sum([l.LayerThickness for l in layers or []])
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
assert bpy.context.scene
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
precision = None
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = prefs.doc.imperial_precision
|
||||
return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
|
||||
|
||||
@classmethod
|
||||
def set_item_name(cls) -> Union[str, None]:
|
||||
@@ -404,7 +407,7 @@ class ObjectMaterialData:
|
||||
material = cls.material
|
||||
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
|
||||
return []
|
||||
return [m.Name for m in material.MaterialConstituents if m.Name]
|
||||
return [m.Name for m in material.MaterialConstituents or [] if m.Name]
|
||||
|
||||
@classmethod
|
||||
def is_type_material_overridden(cls) -> bool:
|
||||
|
||||
@@ -254,6 +254,10 @@ class BIM_PT_object_material(Panel):
|
||||
active_object = bpy.context.active_object
|
||||
self.layerset_bounds(box, active_object, location="Top_Exterior")
|
||||
|
||||
if not ObjectMaterialData.data["set_items"]:
|
||||
row = box.row()
|
||||
row.label(text="No Materials Found")
|
||||
|
||||
for set_item in ObjectMaterialData.data["set_items"]:
|
||||
if (
|
||||
len(self.props.material_set_item_profile_attributes)
|
||||
@@ -357,6 +361,10 @@ class BIM_PT_object_material(Panel):
|
||||
active_object = bpy.context.active_object
|
||||
self.layerset_bounds(box, active_object, location="Top_Interior")
|
||||
|
||||
if not ObjectMaterialData.data["set_items"]:
|
||||
row = box.row()
|
||||
row.label(text="No Materials Found")
|
||||
|
||||
for set_item in ObjectMaterialData.data["set_items"]:
|
||||
material_name = set_item["material"]
|
||||
material_id = set_item["material_id"]
|
||||
|
||||
@@ -193,9 +193,6 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=False,
|
||||
)
|
||||
|
||||
@@ -224,9 +221,6 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -95,15 +95,17 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
|
||||
representation_data["part_of_product"] = None
|
||||
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
|
||||
if fallback_material := (int(props.lining_material) or int(props.framing_material) or int(props.glazing_material)):
|
||||
materials = {
|
||||
"Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material),
|
||||
"Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material),
|
||||
}
|
||||
if props.transom_thickness:
|
||||
materials["Glazing"] = tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material)
|
||||
ifcopenshell.api.material.set_shape_aspect_constituents(
|
||||
ifc_file,
|
||||
element=element,
|
||||
context=body,
|
||||
materials={
|
||||
"Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material),
|
||||
"Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material),
|
||||
"Glazing": tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material),
|
||||
},
|
||||
materials=materials,
|
||||
)
|
||||
elif material := ifcopenshell.util.element.get_material(element):
|
||||
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
|
||||
@@ -143,9 +145,6 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=ifcopenshell.util.representation.get_representation(element, active_context),
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
# type attributes
|
||||
@@ -579,9 +578,6 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
props.is_editing = False
|
||||
|
||||
@@ -186,9 +186,6 @@ class FilledOpeningGenerator:
|
||||
tool.Geometry,
|
||||
obj=voided_obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||
@@ -249,9 +246,6 @@ class FilledOpeningGenerator:
|
||||
tool.Geometry,
|
||||
obj=voided_obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
def generate_opening_from_filling(
|
||||
@@ -420,9 +414,6 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=building_obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -798,9 +789,6 @@ class CloneOpening(Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -580,9 +580,6 @@ def get_generic_product_preview_data(context, relating_type):
|
||||
tool.Geometry,
|
||||
obj_type,
|
||||
representation,
|
||||
should_reload=True,
|
||||
is_global=False,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
context.view_layer.update()
|
||||
break
|
||||
|
||||
@@ -406,9 +406,6 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
elif self.representation_template == "EXTRUSION":
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
@@ -422,9 +419,6 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -453,9 +447,6 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
# Update required as core.type.assign_type may change obj.data
|
||||
@@ -738,7 +729,4 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
@@ -98,7 +98,8 @@ class DumbProfileGenerator:
|
||||
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
|
||||
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
|
||||
coords = (vec1, vec2)
|
||||
profiles.append(self.create_profile_from_2_points(coords))
|
||||
if profile := self.create_profile_from_2_points(coords):
|
||||
profiles.append(profile)
|
||||
return profiles, is_polyline_closed
|
||||
|
||||
def derive_from_cursor(self) -> bpy.types.Object:
|
||||
@@ -166,9 +167,6 @@ class DumbProfileGenerator:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
|
||||
@@ -183,7 +181,7 @@ class DumbProfileGenerator:
|
||||
) -> ProfileFrom2PointsReturn:
|
||||
self.direction = coords[1] - coords[0]
|
||||
length = self.direction.length
|
||||
if round(length, 4) < 0.1:
|
||||
if round(length, 4) < 0.001:
|
||||
return
|
||||
data: dict[str, Any] = {"coords": coords}
|
||||
|
||||
@@ -567,9 +565,6 @@ class DumbProfileJoiner:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
if element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting"):
|
||||
@@ -1036,9 +1031,6 @@ def disable_editing_extrusion_axis(context):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1100,9 +1092,6 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
@@ -373,7 +373,12 @@ class BIMStairProperties(PropertyGroup):
|
||||
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
||||
self["nosing_length"] = 0
|
||||
|
||||
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
|
||||
def update_custom_tread_lock(self, context: bpy.types.Context) -> None:
|
||||
"""When lock is enabled, sync custom treads with tread_run"""
|
||||
if self.custom_tread_lock:
|
||||
self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||
|
||||
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock")
|
||||
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
|
||||
@@ -407,6 +412,12 @@ class BIMStairProperties(PropertyGroup):
|
||||
default="CONCRETE",
|
||||
update=validate_nosing_value,
|
||||
)
|
||||
custom_tread_lock: bpy.props.BoolProperty(
|
||||
name="Lock First/Last Treads to Tread Run",
|
||||
description="When enabled, first and last treads automatically use the Tread Run value",
|
||||
default=True,
|
||||
update=update_custom_tread_lock,
|
||||
)
|
||||
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
|
||||
name="Custom First / Last Treads Widths",
|
||||
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
|
||||
@@ -442,6 +453,7 @@ class BIMStairProperties(PropertyGroup):
|
||||
top_slab_depth: float
|
||||
has_top_nib: bool
|
||||
stair_type: str
|
||||
custom_tread_lock: bool
|
||||
custom_first_last_tread_run: tuple[float, float]
|
||||
nosing_length: float
|
||||
nosing_depth: float
|
||||
@@ -480,8 +492,11 @@ class BIMStairProperties(PropertyGroup):
|
||||
}
|
||||
stair_kwargs.update(generic_props)
|
||||
|
||||
# defined here to appear last in UI
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
# If locked, use tread_run for both first and last treads
|
||||
if self.custom_tread_lock:
|
||||
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||
else:
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
|
||||
if not convert_to_project_units:
|
||||
return stair_kwargs
|
||||
@@ -489,8 +504,24 @@ class BIMStairProperties(PropertyGroup):
|
||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
|
||||
return stair_kwargs
|
||||
|
||||
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
|
||||
"""Get props including custom_tread_lock for saving to IFC"""
|
||||
stair_kwargs = self.get_props_kwargs(convert_to_project_units, stair_type)
|
||||
# Add the lock state for IFC storage (after getting base kwargs to avoid passing to generate function)
|
||||
stair_kwargs["custom_tread_lock"] = self.custom_tread_lock
|
||||
return stair_kwargs
|
||||
|
||||
def set_props_kwargs_from_ifc_data(self, kwargs):
|
||||
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
|
||||
|
||||
# Determine lock state based on whether custom treads match tread_run
|
||||
# If custom_tread_lock wasn't saved (old files), infer it from the data
|
||||
if "custom_tread_lock" not in kwargs:
|
||||
custom_treads = kwargs.get("custom_first_last_tread_run", (0.0, 0.0))
|
||||
tread_run = kwargs.get("tread_run", 0.3)
|
||||
# Lock is off if either custom tread differs from tread_run and is not 0
|
||||
kwargs["custom_tread_lock"] = not any(ct != 0.0 and ct != tread_run for ct in custom_treads)
|
||||
|
||||
for prop_name in kwargs:
|
||||
setattr(self, prop_name, kwargs[prop_name])
|
||||
|
||||
|
||||
@@ -538,9 +538,6 @@ def cancel_editing_railing_path(context: bpy.types.Context) -> set[str]:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -167,9 +167,6 @@ class DumbSlabGenerator:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
obj.matrix_world = obj.matrix_world @ Matrix.Rotation(self.x_angle, 4, "X")
|
||||
|
||||
@@ -360,9 +357,6 @@ class DumbSlabPlaner:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_rep,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
bonsai.core.geometry.remove_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||
@@ -386,9 +380,6 @@ class DumbSlabPlaner:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -588,9 +579,6 @@ class EditSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -632,9 +620,6 @@ def disable_editing_extrusion_profile(context):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -767,9 +752,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
# Only certain classes should have a footprint
|
||||
|
||||
@@ -188,7 +188,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
stair_data = props.get_props_kwargs(convert_to_project_units=True)
|
||||
# Use the special method that includes custom_tread_lock for IFC storage
|
||||
stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair")
|
||||
@@ -241,7 +242,8 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
assert element
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
|
||||
data = props.get_props_kwargs(convert_to_project_units=True)
|
||||
# Use the special method that includes custom_tread_lock for IFC storage
|
||||
data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
|
||||
props.is_editing = False
|
||||
regenerate_stair_mesh(obj)
|
||||
tool.Model.add_body_representation(obj)
|
||||
|
||||
@@ -302,13 +302,38 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_name in props.get_props_kwargs():
|
||||
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
|
||||
if prop_name == "custom_tread_lock":
|
||||
continue
|
||||
|
||||
prop_value = getattr(props, prop_name)
|
||||
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||
|
||||
# Special handling for custom_first_last_tread_run
|
||||
if prop_name == "custom_first_last_tread_run":
|
||||
# Draw the lock toggle
|
||||
row_lock = self.layout.row(align=True)
|
||||
lock_text = (
|
||||
"Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
|
||||
)
|
||||
row_lock.prop(
|
||||
props,
|
||||
"custom_tread_lock",
|
||||
text=lock_text,
|
||||
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
|
||||
)
|
||||
|
||||
# Only show the custom values input if unlocked
|
||||
if not props.custom_tread_lock:
|
||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||
self.layout.label(text=f"{prop_readable_name}:")
|
||||
self.layout.prop(props, prop_name, text="")
|
||||
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||
self.layout.label(text=f"{prop_readable_name}:")
|
||||
self.layout.prop(props, prop_name, text="")
|
||||
else:
|
||||
self.layout.prop(props, prop_name)
|
||||
|
||||
if prop_name == "height": # Weak but we just want to insert this inside props drawing
|
||||
row_length = self.layout.row(align=True)
|
||||
row_length.prop(props, "total_length_target")
|
||||
@@ -550,7 +575,7 @@ class BIM_PT_door(bpy.types.Panel):
|
||||
|
||||
row = prop_with_search(self.layout, props, "lining_material")
|
||||
tool.Model.draw_material_ui_select(row, props.lining_material)
|
||||
row = prop_with_search(self.layout, props, "framing_material", text="Panel Material")
|
||||
row = prop_with_search(self.layout, props, "framing_material", text="Panel Framing Material")
|
||||
tool.Model.draw_material_ui_select(row, props.framing_material)
|
||||
if props.transom_thickness:
|
||||
row = prop_with_search(self.layout, props, "glazing_material")
|
||||
|
||||
@@ -517,9 +517,6 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
# Object rotation
|
||||
@@ -1024,9 +1021,6 @@ class DumbWallGenerator:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
|
||||
|
||||
@@ -123,9 +123,6 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None:
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=ifcopenshell.util.representation.get_representation(element, active_context),
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
|
||||
# type attributes
|
||||
@@ -495,9 +492,6 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
props.is_editing = False
|
||||
|
||||
@@ -155,6 +155,7 @@ class CreateProject(bpy.types.Operator):
|
||||
tool.Ifc, tool.Georeference, tool.Project, tool.Spatial, schema=props.export_schema, template=template
|
||||
)
|
||||
tool.Blender.register_toolbar()
|
||||
tool.Loader.set_unit_scale(ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()))
|
||||
|
||||
def rollback(self, data):
|
||||
IfcStore.file = None
|
||||
@@ -2521,6 +2522,7 @@ class FlipClippingPlane(bpy.types.Operator):
|
||||
obj = context.active_object
|
||||
if obj in tool.Project.get_project_props().clipping_planes_objs:
|
||||
obj.rotation_euler[0] += radians(180)
|
||||
obj.rotation_euler[0] %= radians(360)
|
||||
context.view_layer.update()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -450,9 +450,9 @@ class BIMProjectProperties(PropertyGroup):
|
||||
self.mvd = header_data.mvd
|
||||
self.author_name = header_data.author_name
|
||||
self.author_email = header_data.author_email
|
||||
self.organisation_name = header_data.organization_name
|
||||
self.organisation_email = header_data.organization_email
|
||||
self.authorisation = header_data.authorization
|
||||
self.organisation_name = header_data.organisation_name
|
||||
self.organisation_email = header_data.organisation_email
|
||||
self.authorisation = header_data.authorisation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
|
||||
@@ -653,4 +653,5 @@ class BIM_PT_purge(Panel):
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
|
||||
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
|
||||
row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = object_type
|
||||
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
|
||||
merge_op.object_type = object_type
|
||||
|
||||
@@ -242,14 +242,11 @@ class BIM_PT_object_psets(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(obj)
|
||||
if not ifc_id:
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(ifc_id):
|
||||
return False
|
||||
return True
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcObjectDefinition")
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectPsetsData.is_loaded:
|
||||
@@ -388,14 +385,11 @@ class BIM_PT_object_qtos(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(obj)
|
||||
if not ifc_id:
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(ifc_id):
|
||||
return False
|
||||
return True
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcObjectDefinition")
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectQtosData.is_loaded:
|
||||
|
||||
@@ -317,6 +317,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
|
||||
)
|
||||
|
||||
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
|
||||
element = core.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
@@ -326,16 +327,9 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
predefined_type=predefined_type,
|
||||
should_add_representation=False,
|
||||
)
|
||||
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -579,9 +573,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
if not tool.Ifc.get_entity(props.representation_obj):
|
||||
bpy.data.objects.remove(props.representation_obj)
|
||||
@@ -601,9 +592,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
elif representation_template == "EXTRUSION":
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
@@ -650,9 +638,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
elif representation_template in ("LAYERSET_AXIS2", "LAYERSET_AXIS3"):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
@@ -763,9 +748,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
elif representation_template == "EDGE":
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
@@ -778,9 +760,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
elif representation_template == "FACE":
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
@@ -793,9 +772,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
bpy.context.view_layer.update() # Ensures obj.matrix_world is correct
|
||||
|
||||
@@ -101,7 +101,7 @@ class ColourByPropertyData:
|
||||
elif pset.endswith("BaseQuantities"):
|
||||
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
|
||||
else:
|
||||
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
|
||||
keys.extend([f'"{pset}"."{name}"' for name in properties.keys() if name != "id"])
|
||||
results = [(k, k, "") for k in keys]
|
||||
return default + results
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ classes = (
|
||||
operator.RemoveWorkTime,
|
||||
operator.ReorderTask,
|
||||
operator.SaveAnimationColorScheme,
|
||||
operator.SelectTaskElements,
|
||||
operator.SelectTaskRelatedInputs,
|
||||
operator.SelectTaskRelatedProducts,
|
||||
operator.SelectUnassignedWorkScheduleProducts,
|
||||
|
||||
@@ -243,6 +243,9 @@ class SequenceData:
|
||||
data["NestingIndex"] = None
|
||||
for rel in task.Nests or []:
|
||||
data["NestingIndex"] = rel.RelatedObjects.index(task)
|
||||
data["TotalInputs"] = len(tool.Sequence.get_task_inputs(task))
|
||||
data["TotalOutputs"] = len(tool.Sequence.get_task_outputs(task))
|
||||
data["TotalElements"] = data["TotalInputs"] + data["TotalOutputs"]
|
||||
cls.data["tasks"][task.id()] = data
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1281,6 +1281,17 @@ class DisableEditingSequence(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectTaskElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.select_task_elements"
|
||||
bl_label = "Select All Task Elements"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
task: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.select_task_inputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
|
||||
core.select_task_outputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
|
||||
|
||||
|
||||
class SelectTaskRelatedProducts(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.select_task_related_products"
|
||||
bl_label = "Select All Output Products"
|
||||
|
||||
@@ -75,6 +75,8 @@ def update_active_task_index(self, context):
|
||||
bonsai.bim.module.pset.data.refresh()
|
||||
if self.editing_task_type == "SEQUENCE":
|
||||
tool.Sequence.load_task_properties()
|
||||
if self.should_auto_select:
|
||||
tool.Sequence.select_active_task_elements(task)
|
||||
|
||||
|
||||
def update_active_task_outputs(self, context):
|
||||
@@ -485,6 +487,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
other_columns: EnumProperty(
|
||||
items=[
|
||||
("Controls.Calendar", "Calendar", ""),
|
||||
("Controls.TotalElements", "Total Elements", "The total number of assigned elements to this task"),
|
||||
("Controls.TotalInputs", "Total Inputs", "The total number of input elements to this task"),
|
||||
("Controls.TotalOutputs", "Total Outputs", "The total number of input elements to this task"),
|
||||
],
|
||||
name="Special Columns",
|
||||
)
|
||||
@@ -518,9 +523,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
active_task_input_index: IntProperty(name="Active Task Input Index")
|
||||
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
|
||||
active_task_output_index: IntProperty(name="Active Task Output Index")
|
||||
show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs)
|
||||
show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources)
|
||||
show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs)
|
||||
show_nested_outputs: BoolProperty(
|
||||
name="Show Nested Task Elements", default=False, update=update_active_task_outputs
|
||||
)
|
||||
show_nested_resources: BoolProperty(
|
||||
name="Show Nested Task Elements", default=False, update=update_active_task_resources
|
||||
)
|
||||
show_nested_inputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_inputs)
|
||||
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
|
||||
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
|
||||
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
|
||||
@@ -531,6 +540,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
filter_by_active_schedule: BoolProperty(
|
||||
name="Filter By Active Schedule", default=False, update=update_filter_by_active_schedule
|
||||
)
|
||||
should_auto_select: BoolProperty(
|
||||
name="Auto Select", description="Auto select elements when task is selected", default=False
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
work_schedule_predefined_types: str
|
||||
|
||||
@@ -290,13 +290,21 @@ class BIM_PT_work_schedules(Panel):
|
||||
row2 = self.layout.row(align=True)
|
||||
row2.alignment = "RIGHT"
|
||||
|
||||
row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
|
||||
row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
|
||||
row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
|
||||
row2.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
|
||||
col = row2.column()
|
||||
row_ = col.row(align=True)
|
||||
op = row_.operator("bim.select_task_elements", icon="RESTRICT_SELECT_OFF", text="Select Elements")
|
||||
op.task = task.ifc_definition_id
|
||||
row_.prop(self.props, "should_auto_select", icon="OBJECT_HIDDEN", text="")
|
||||
|
||||
col = row2.column()
|
||||
row_ = col.row(align=True)
|
||||
row_.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
|
||||
row_.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
|
||||
row_.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
|
||||
row_.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
|
||||
ifc_definition_id
|
||||
)
|
||||
row2.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
|
||||
row_.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
|
||||
ifc_definition_id
|
||||
)
|
||||
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
|
||||
@@ -716,11 +724,10 @@ class BIM_PT_task_icom(Panel):
|
||||
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
|
||||
op.related_object = input_id
|
||||
|
||||
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
|
||||
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.task = task.ifc_definition_id
|
||||
row2.prop(self.props, "show_nested_inputs", icon="OUTLINER", text="")
|
||||
|
||||
row2 = col.row()
|
||||
row2.prop(self.props, "show_nested_inputs", text="Show Nested")
|
||||
row2 = col.row()
|
||||
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
|
||||
|
||||
@@ -744,8 +751,7 @@ class BIM_PT_task_icom(Panel):
|
||||
op.related_object_type = "RESOURCE"
|
||||
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
|
||||
|
||||
row2 = col.row()
|
||||
row2.prop(self.props, "show_nested_resources", text="Show Nested")
|
||||
row2.prop(self.props, "show_nested_resources", icon="OUTLINER", text="")
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
@@ -773,10 +779,10 @@ class BIM_PT_task_icom(Panel):
|
||||
output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
|
||||
op.relating_product = output_id
|
||||
|
||||
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select")
|
||||
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.task = task.ifc_definition_id
|
||||
row2 = col.row()
|
||||
row2.prop(self.props, "show_nested_outputs", text="Show Nested")
|
||||
row2.prop(self.props, "show_nested_outputs", icon="OUTLINER", text="")
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
|
||||
@@ -893,7 +899,7 @@ class BIM_UL_tasks(UIList):
|
||||
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
|
||||
split2.prop(item, "name", emboss=False, text="")
|
||||
|
||||
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task)
|
||||
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task, item.ifc_definition_id)
|
||||
|
||||
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
|
||||
row.prop(
|
||||
@@ -960,6 +966,7 @@ class BIM_UL_tasks(UIList):
|
||||
row: bpy.types.UILayout,
|
||||
item: Optional[bpy.types.PropertyGroup] = None,
|
||||
task: Optional[dict[str, Any]] = None,
|
||||
ifc_definition_id: int = 0,
|
||||
*,
|
||||
header: bool = False,
|
||||
) -> None:
|
||||
@@ -999,6 +1006,21 @@ class BIM_UL_tasks(UIList):
|
||||
row.label(text=item.derived_calendar + "*")
|
||||
else:
|
||||
row.label(text=item.calendar or "-")
|
||||
elif column.name == "Controls.TotalInputs":
|
||||
if header:
|
||||
row.label(text="# Inputs")
|
||||
else:
|
||||
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalInputs"]))
|
||||
elif column.name == "Controls.TotalOutputs":
|
||||
if header:
|
||||
row.label(text="# Outputs")
|
||||
else:
|
||||
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalOutputs"]))
|
||||
elif column.name == "Controls.TotalElements":
|
||||
if header:
|
||||
row.label(text="# Elements")
|
||||
else:
|
||||
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalElements"]))
|
||||
else:
|
||||
ifc_class, name = column.name.split(".")
|
||||
if header:
|
||||
|
||||
@@ -148,14 +148,14 @@ class SpatialDecompositionData:
|
||||
elif tool.Ifc.get_schema() == "IFC4X3":
|
||||
results = {
|
||||
"IfcBridge": ["IfcBridge", "IfcBridgePart", "IfcSpace"],
|
||||
"IfcBridgePart": ["IfcSpace"],
|
||||
"IfcBridgePart": ["IfcBridgePart", "IfcSpace"],
|
||||
"IfcBuilding": ["IfcBuilding", "IfcBuildingStorey", "IfcSpace"],
|
||||
"IfcBuildingStorey": ["IfcBuildingStorey", "IfcSpace"],
|
||||
"IfcExternalSpatialElement": ["IfcExternalSpatialElement"],
|
||||
"IfcFacility": ["IfcFacility", "IfcFacilityPartCommon", "IfcSpace"],
|
||||
"IfcFacilityPartCommon": ["IfcSpace"],
|
||||
"IfcFacilityPartCommon": ["IfcFacilityPartCommon", "IfcSpace"],
|
||||
"IfcMarineFacility": ["IfcMarineFacility", "IfcMarinePart", "IfcSpace"],
|
||||
"IfcMarinePart": ["IfcSpace"],
|
||||
"IfcMarinePart": ["IfcMarinePart", "IfcSpace"],
|
||||
"IfcProject": [
|
||||
"IfcBridge",
|
||||
"IfcBuilding",
|
||||
@@ -168,9 +168,9 @@ class SpatialDecompositionData:
|
||||
"IfcSpace",
|
||||
],
|
||||
"IfcRailway": ["IfcRailway", "IfcRailwayPart", "IfcSpace"],
|
||||
"IfcRailwayPart": ["IfcSpace"],
|
||||
"IfcRailwayPart": ["IfcRailwayPart", "IfcSpace"],
|
||||
"IfcRoad": ["IfcRoad", "IfcRoadPart", "IfcSpace"],
|
||||
"IfcRoadPart": ["IfcSpace"],
|
||||
"IfcRoadPart": ["IfcRoadPart", "IfcSpace"],
|
||||
"IfcSite": [
|
||||
"IfcBridge",
|
||||
"IfcBuilding",
|
||||
|
||||
@@ -70,7 +70,7 @@ class GridDecorator:
|
||||
|
||||
grid_props = tool.Spatial.get_grid_props()
|
||||
for axis in grid_props.grid_axes:
|
||||
if not (obj := axis.obj) or obj.hide_get() == True:
|
||||
if not (obj := axis.obj) or obj.visible_get() is False:
|
||||
continue
|
||||
if obj.select_get() and context.mode != "OBJECT":
|
||||
continue
|
||||
@@ -125,21 +125,22 @@ class GridDecorator:
|
||||
unselected_edges = []
|
||||
grid_props = tool.Spatial.get_grid_props()
|
||||
for axis in grid_props.grid_axes:
|
||||
if (obj := axis.obj) and obj.hide_get() == False:
|
||||
if obj.select_get():
|
||||
if context.mode != "OBJECT":
|
||||
continue
|
||||
edges = selected_edges
|
||||
verts = selected_verts
|
||||
else:
|
||||
edges = unselected_edges
|
||||
verts = unselected_verts
|
||||
i = len(verts)
|
||||
edges.append([i, i + 1])
|
||||
matrix_world = obj.matrix_world
|
||||
v1 = matrix_world @ obj.data.vertices[0].co
|
||||
v2 = matrix_world @ obj.data.vertices[1].co
|
||||
verts.extend([v1, v2])
|
||||
if not (obj := axis.obj) or obj.visible_get() is False:
|
||||
continue
|
||||
if obj.select_get():
|
||||
if context.mode != "OBJECT":
|
||||
continue
|
||||
edges = selected_edges
|
||||
verts = selected_verts
|
||||
else:
|
||||
edges = unselected_edges
|
||||
verts = unselected_verts
|
||||
i = len(verts)
|
||||
edges.append([i, i + 1])
|
||||
matrix_world = obj.matrix_world
|
||||
v1 = matrix_world @ obj.data.vertices[0].co
|
||||
v2 = matrix_world @ obj.data.vertices[1].co
|
||||
verts.extend([v1, v2])
|
||||
|
||||
if unselected_verts:
|
||||
self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges)
|
||||
|
||||
@@ -550,9 +550,7 @@ class ToggleGrids(bpy.types.Operator):
|
||||
is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"):
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
obj.hide_set(not self.is_visible)
|
||||
tool.Spatial.set_grid_visibility(self.is_visible)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -564,11 +562,5 @@ class ToggleSpatialElements(bpy.types.Operator):
|
||||
is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
|
||||
else:
|
||||
elements = tool.Ifc.get().by_type("IfcSpatialElement")
|
||||
for element in elements:
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
obj.hide_set(not self.is_visible)
|
||||
tool.Spatial.set_space_visibility(self.is_visible)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -224,7 +224,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
|
||||
is_visible: BoolProperty(
|
||||
name="Is Visible",
|
||||
description="Show or hide spatial elements, such as buildings, sites, etc",
|
||||
default=True,
|
||||
default=False,
|
||||
update=update_spatial_is_visible,
|
||||
)
|
||||
container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"})
|
||||
|
||||
@@ -135,6 +135,7 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
|
||||
try:
|
||||
request_id = data.get("id")
|
||||
ids_string = data.get("ids")
|
||||
props = tool.Tester.get_tester_props()
|
||||
|
||||
if not request_id:
|
||||
await self.emit("error", {"error": "No request ID provided"}, room=sid)
|
||||
@@ -176,7 +177,7 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
|
||||
json_report = json_reporter.to_string()
|
||||
|
||||
# HTML report
|
||||
html_reporter = ifctester.reporter.Html(ids)
|
||||
html_reporter = ifctester.reporter.Html(ids, hide_skipped=props.hide_skipped_specs)
|
||||
html_reporter.report()
|
||||
html_report = html_reporter.to_string()
|
||||
|
||||
@@ -267,7 +268,7 @@ class ExecuteIfcTester(bpy.types.Operator):
|
||||
start = time.time()
|
||||
|
||||
if props.generate_html_report:
|
||||
engine = ifctester.reporter.Html(specs)
|
||||
engine = ifctester.reporter.Html(specs, props.hide_skipped_specs)
|
||||
engine.report()
|
||||
output_path = output.as_posix()
|
||||
engine.to_file(output_path)
|
||||
@@ -337,9 +338,21 @@ class StartIfcTesterWebapp(bpy.types.Operator):
|
||||
websocket_server_thread = threading.Thread(target=run_websocket_server, daemon=True)
|
||||
websocket_server_thread.start()
|
||||
|
||||
py_version = sys.version_info
|
||||
bonsai_lib_path = (
|
||||
Path(bpy.utils.user_resource("EXTENSIONS"))
|
||||
/ ".local"
|
||||
/ "lib"
|
||||
/ f"python{py_version.major}.{py_version.minor}"
|
||||
/ "site-packages"
|
||||
)
|
||||
env = os.environ.copy()
|
||||
env["BONSAI_LIB_PATH"] = str(bonsai_lib_path)
|
||||
env["BONSAI_VERSION"] = tool.Blender.get_bonsai_version()
|
||||
|
||||
# Start the Flask server as subprocess
|
||||
webapp_process = subprocess.Popen(
|
||||
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)]
|
||||
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)], env=env
|
||||
)
|
||||
|
||||
# Update properties
|
||||
@@ -472,6 +485,7 @@ class SelectRequirement(bpy.types.Operator):
|
||||
props.n_entities = len(failed_entities)
|
||||
props.has_entities = True if props.n_entities > 0 else False
|
||||
props.failed_entities.clear()
|
||||
props.active_requirement_index = self.req_index
|
||||
|
||||
for e in failed_entities:
|
||||
new_entity = props.failed_entities.add()
|
||||
|
||||
@@ -71,6 +71,8 @@ class IfcTesterProperties(PropertyGroup):
|
||||
generate_ods_report: BoolProperty(default=False, name="Generate ODS report", options=set())
|
||||
flag: BoolProperty(default=False, name="Flag Failed Entities", options=set())
|
||||
active_specification_index: IntProperty(name="Active Specification Index", update=update_active_specification_index)
|
||||
active_requirement_index: IntProperty(name="Active Requirement Index")
|
||||
|
||||
old_index: IntProperty(name="", default=0)
|
||||
active_failed_entity_index: IntProperty(name="Active Failed Entity Index")
|
||||
specifications: CollectionProperty(name="Specifications", type=Specification)
|
||||
@@ -80,6 +82,7 @@ class IfcTesterProperties(PropertyGroup):
|
||||
webapp_server_port: IntProperty(name="Webapp Server Port", default=0)
|
||||
webapp_is_running: BoolProperty(default=False, name="Webapp Is Running", options=set())
|
||||
websocket_server_port: IntProperty(name="WebSocket Server Port", default=0)
|
||||
hide_skipped_specs: BoolProperty(default=False, name="Hide skipped Specifications", options=set())
|
||||
|
||||
if TYPE_CHECKING:
|
||||
specs: MultipleFileSelect
|
||||
@@ -89,6 +92,7 @@ class IfcTesterProperties(PropertyGroup):
|
||||
generate_ods_report: bool
|
||||
flag: bool
|
||||
active_specification_index: int
|
||||
active_requirement_index: int
|
||||
old_index: int
|
||||
active_failed_entity_index: int
|
||||
specifications: bpy.types.bpy_prop_collection_idprop[Specification]
|
||||
|
||||
@@ -20,6 +20,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bonsai.bim.ui import draw_multiline_text
|
||||
|
||||
import bpy
|
||||
from bpy.types import Panel, UIList
|
||||
import bonsai.tool as tool
|
||||
@@ -60,7 +62,8 @@ class BIM_PT_tester(Panel):
|
||||
row.prop(props, "generate_ods_report")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "flag")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "hide_skipped_specs")
|
||||
if not tool.Ifc.get() or not props.should_load_from_memory:
|
||||
row = self.layout.row(align=True)
|
||||
props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)")
|
||||
@@ -95,13 +98,16 @@ class BIM_PT_tester(Panel):
|
||||
"active_specification_index",
|
||||
)
|
||||
|
||||
self.draw_editable_ui()
|
||||
self.draw_editable_ui(context)
|
||||
row = self.layout.row()
|
||||
row.operator("bim.export_bcf", text="Export BCF", icon="EXPORT")
|
||||
|
||||
def draw_editable_ui(self) -> None:
|
||||
def draw_editable_ui(self, context: bpy.types.Context) -> None:
|
||||
props = tool.Tester.get_tester_props()
|
||||
specification = TesterData.data["specification"]
|
||||
is_skipped = specification["total_checks"] == 0 and specification["cardinality"] == "optional"
|
||||
if props.hide_skipped_specs and is_skipped:
|
||||
return
|
||||
|
||||
n_requirements = len(specification["requirements"])
|
||||
|
||||
@@ -110,6 +116,13 @@ class BIM_PT_tester(Panel):
|
||||
row.label(
|
||||
text=f'Passed: {specification["total_checks_pass"]}/{specification["total_checks"]} ({specification["percent_checks_pass"]}%)'
|
||||
)
|
||||
row = self.layout.row()
|
||||
if specification.get("instructions"):
|
||||
row.label(text="Instructions:")
|
||||
box = self.layout.box()
|
||||
column = box.column(align=True)
|
||||
draw_multiline_text(column, specification.get("instructions"), context=context)
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text=f"Requirements ({n_requirements}):")
|
||||
if props.flag:
|
||||
@@ -132,6 +145,16 @@ class BIM_PT_tester(Panel):
|
||||
and props.n_entities > 0
|
||||
and len(props.failed_entities) > 0
|
||||
):
|
||||
|
||||
requirement = specification["requirements"][props.active_requirement_index]
|
||||
metadata = requirement.get("metadata")
|
||||
if metadata and metadata.get("@instructions"):
|
||||
row = self.layout.row()
|
||||
row.label(text="Instructions:")
|
||||
box = self.layout.box()
|
||||
column = box.column(align=True)
|
||||
draw_multiline_text(column, metadata.get("@instructions"), context=context)
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text=f"Failed entities [{props.n_entities}]:")
|
||||
self.layout.template_list(
|
||||
@@ -160,6 +183,34 @@ class BIM_UL_tester_specifications(UIList):
|
||||
row.label(text=item.name, icon="CHECKMARK" if item.status else "CANCEL")
|
||||
row.label(text=item.description)
|
||||
|
||||
def filter_items(self, context: bpy.types.Context, data: IfcTesterProperties, propname: str):
|
||||
items = getattr(data, propname)
|
||||
filter_flags = [self.bitflag_filter_item] * len(items)
|
||||
|
||||
props = tool.Tester.get_tester_props()
|
||||
if props.hide_skipped_specs:
|
||||
for idx, item in enumerate(items):
|
||||
report = tool.Tester.report[idx]
|
||||
if report["total_checks"] != 0:
|
||||
filter_flags[idx] |= self.bitflag_filter_item
|
||||
else:
|
||||
filter_flags[idx] &= ~self.bitflag_filter_item
|
||||
|
||||
filter_name = self.filter_name
|
||||
if filter_name:
|
||||
name_filtered = bpy.types.UI_UL_list.filter_items_by_name(
|
||||
filter_name,
|
||||
self.bitflag_filter_item,
|
||||
items,
|
||||
"name",
|
||||
)
|
||||
if len(name_filtered) == len(filter_flags):
|
||||
for idx, flag in enumerate(name_filtered):
|
||||
if flag == 0:
|
||||
filter_flags[idx] &= ~self.bitflag_filter_item
|
||||
|
||||
return filter_flags, []
|
||||
|
||||
|
||||
class BIM_UL_tester_failed_entities(UIList):
|
||||
def draw_item(
|
||||
|
||||
@@ -121,9 +121,6 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_active_representation,
|
||||
should_reload=False,
|
||||
is_global=False,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -34,14 +34,11 @@ class BIM_PT_type(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return False
|
||||
if not element.is_a("IfcProduct") and not element.is_a("IfcTypeProduct"):
|
||||
return True
|
||||
return True
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and (element.is_a("IfcObject") or element.is_a("IfcTypeObject"))
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
if not TypeData.is_loaded:
|
||||
|
||||
@@ -148,11 +148,6 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=voided_obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
# Don't sync changes because object has an opening,
|
||||
# therefore bim.update_representaiton wouldn't work either way.
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Geometry.lock_scale(voided_obj)
|
||||
|
||||
@@ -196,9 +191,6 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=building_obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Geometry.unlock_scale_object_with_openings(obj)
|
||||
tool.Geometry.clear_cache(element)
|
||||
|
||||
@@ -40,6 +40,7 @@ from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
|
||||
from bonsai.bim.module.pset.prop import IfcProperty
|
||||
from typing import Optional, TYPE_CHECKING, Literal
|
||||
from natsort import natsorted
|
||||
import textwrap
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -373,11 +374,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
description="Path to create and store temporary files. If left blank, a system default will be used.",
|
||||
subtype="DIR_PATH",
|
||||
)
|
||||
spatial_elements_unselectable: BoolProperty(
|
||||
name="Make Spatial Elements Unselectable By Default",
|
||||
default=True,
|
||||
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences",
|
||||
)
|
||||
decorations_colour: bpy.props.FloatVectorProperty(
|
||||
name="Decorations Color", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
||||
)
|
||||
@@ -503,7 +499,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
activate_workspace: bool
|
||||
should_setup_toolbar: bool
|
||||
should_play_chaching_sound: bool
|
||||
spatial_elements_unselectable: bool
|
||||
tmp_dir: str
|
||||
decorations_colour: tuple[float, float, float, float]
|
||||
decorator_color_selected: tuple[float, float, float, float]
|
||||
@@ -565,7 +560,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
layout.prop(self, "should_setup_toolbar")
|
||||
layout.prop(self, "should_use_snap")
|
||||
layout.prop(self, "should_play_chaching_sound")
|
||||
layout.prop(self, "spatial_elements_unselectable")
|
||||
|
||||
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "occurrence_name_style")
|
||||
@@ -1498,6 +1492,31 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
|
||||
url_op.uri = url
|
||||
|
||||
|
||||
def draw_multiline_text(
|
||||
layout: bpy.types.UILayout,
|
||||
text: str,
|
||||
*,
|
||||
context: bpy.types.Context | None = None,
|
||||
) -> None:
|
||||
"""Render a read-only text box that wraps long text."""
|
||||
assert layout
|
||||
|
||||
region_width = 200
|
||||
if context and context.region:
|
||||
region_width = context.region.width
|
||||
|
||||
approximate_char_width = 7 # Empirical average width for Blender UI font (px)
|
||||
wrap_width = max(20, int(region_width / approximate_char_width))
|
||||
|
||||
for paragraph in text.splitlines():
|
||||
if not paragraph:
|
||||
layout.label(text="")
|
||||
continue
|
||||
|
||||
for line in textwrap.wrap(paragraph, width=wrap_width):
|
||||
layout.label(text=line)
|
||||
|
||||
|
||||
class BIM_PT_decorators_overlay(Panel):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "HEADER"
|
||||
|
||||
@@ -39,7 +39,7 @@ def disable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) ->
|
||||
def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
|
||||
drawing.synchronise_ifc_and_text_attributes(obj)
|
||||
drawing.update_text_size_pset(obj)
|
||||
drawing.update_newline_at_and_symbol(obj)
|
||||
drawing.update_text_annotation_properties(obj)
|
||||
drawing.disable_editing_text(obj)
|
||||
|
||||
|
||||
@@ -108,10 +108,6 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
|
||||
def regenerate_sheet(
|
||||
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
|
||||
) -> Union[list[tool.Drawing.SheetWarningType], None]:
|
||||
warnings = drawing.validate_sheet_files(sheet)
|
||||
if warnings:
|
||||
return warnings
|
||||
|
||||
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
|
||||
assert titleblock_uri
|
||||
|
||||
@@ -320,6 +316,7 @@ def duplicate_drawing(
|
||||
) -> ifcopenshell.entity_instance:
|
||||
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
|
||||
new_drawing = ifc.run("root.copy_class", product=drawing)
|
||||
drawing_tool.clear_annotation_relationships(new_drawing)
|
||||
drawing_tool.copy_representation(drawing, new_drawing)
|
||||
drawing_tool.set_name(new_drawing, drawing_name)
|
||||
group = drawing_tool.get_drawing_group(new_drawing)
|
||||
@@ -440,8 +437,7 @@ def add_annotation(
|
||||
drawing_tool.show_decorations()
|
||||
obj = drawing_tool.create_annotation_object(drawing, object_type)
|
||||
element = ifc.get_entity(obj)
|
||||
# TODO: element is never None?
|
||||
if not element:
|
||||
if not element: # Brand new annotation
|
||||
relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None
|
||||
element = drawing_tool.run_root_assign_class(
|
||||
obj=obj,
|
||||
@@ -476,46 +472,44 @@ def sync_references(
|
||||
if not drawing_tool.has_annotation(drawing):
|
||||
return
|
||||
|
||||
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))
|
||||
if not context:
|
||||
if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))):
|
||||
return
|
||||
|
||||
group = drawing_tool.get_drawing_group(drawing)
|
||||
for reference_element in drawing_tool.get_potential_reference_elements(drawing):
|
||||
potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing)
|
||||
|
||||
for element in potential_reference_elements:
|
||||
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
|
||||
drawing_tool.sync_object_placement(obj)
|
||||
|
||||
for element in drawing_tool.get_group_elements(group):
|
||||
if not drawing_tool.is_auto_annotation(element):
|
||||
continue
|
||||
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
|
||||
drawing_tool.sync_object_placement(obj)
|
||||
if not (reference_element := drawing_tool.get_assigned_product(element)):
|
||||
if obj := ifc.get_object(element):
|
||||
drawing_tool.delete_object(obj)
|
||||
ifc.run("root.remove_product", product=element)
|
||||
continue
|
||||
reference_obj = ifc.get_object(reference_element)
|
||||
annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element)
|
||||
if reference_element not in potential_reference_elements:
|
||||
# It was auto created, so it makes sense to auto delete
|
||||
if obj := ifc.get_object(element):
|
||||
drawing_tool.delete_object(obj)
|
||||
ifc.run("root.remove_product", product=element)
|
||||
elif not drawing_tool.regenerate_reference_annotation(drawing, element, reference_element, context):
|
||||
if obj := ifc.get_object(element):
|
||||
drawing_tool.delete_object(obj)
|
||||
ifc.run("root.remove_product", product=element)
|
||||
|
||||
should_delete_existing_annotation = False
|
||||
should_create_annotation = False
|
||||
|
||||
# remove annotation only if the reference object was changed
|
||||
# otherwise we rely on the existing annotation
|
||||
if annotation:
|
||||
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
|
||||
should_delete_existing_annotation = True
|
||||
|
||||
if should_delete_existing_annotation or not annotation:
|
||||
should_create_annotation = True
|
||||
|
||||
if should_delete_existing_annotation:
|
||||
annotation_obj = ifc.get_object(annotation)
|
||||
if annotation_obj:
|
||||
drawing_tool.delete_object(annotation_obj)
|
||||
ifc.run("root.remove_product", product=annotation)
|
||||
|
||||
if should_create_annotation:
|
||||
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
|
||||
if annotation:
|
||||
for reference_element in potential_reference_elements:
|
||||
if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
|
||||
if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
|
||||
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
|
||||
ifc.run("group.assign_group", group=group, products=[annotation])
|
||||
collector.assign(ifc.get_object(annotation))
|
||||
|
||||
if reference_obj and ifc.is_moved(reference_obj):
|
||||
drawing_tool.sync_object_placement(reference_obj)
|
||||
|
||||
if reference_obj and ifc.is_edited(reference_obj):
|
||||
drawing_tool.sync_object_representation(reference_obj)
|
||||
|
||||
|
||||
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
|
||||
drawing.select_assigned_product(context)
|
||||
|
||||
@@ -115,24 +115,9 @@ def switch_representation(
|
||||
geometry: type[tool.Geometry],
|
||||
obj: bpy.types.Object,
|
||||
representation: ifcopenshell.entity_instance,
|
||||
should_reload: bool = True,
|
||||
is_global: bool = True,
|
||||
should_sync_changes_first: bool = False,
|
||||
apply_openings: bool = True,
|
||||
) -> None:
|
||||
"""Function can switch to representation that wasn't yet assigned to that object. See #2766.
|
||||
|
||||
:param should_sync_changes_first: sync ifc representation with current state of `obj.data`
|
||||
:param should_reload: reload `obj.data` from ifc representation
|
||||
:param is_global: replace mesh data for all users of `obj.data`, not just `obj`
|
||||
|
||||
"""
|
||||
if should_sync_changes_first and ifc.is_edited(obj) and not geometry.is_box_representation(representation):
|
||||
representation_id = geometry.get_representation_id(representation)
|
||||
geometry.run_geometry_update_representation(obj=obj)
|
||||
if not geometry.does_representation_id_exist(representation_id):
|
||||
return
|
||||
|
||||
"""Switch obj representation (even if not yet assigned to that object. See #2766.)"""
|
||||
if not geometry.get_object_data(obj) and geometry.is_text_literal(representation):
|
||||
return
|
||||
|
||||
@@ -142,9 +127,7 @@ def switch_representation(
|
||||
geometry.reimport_element_representations(obj, representation, apply_openings=apply_openings)
|
||||
|
||||
|
||||
def get_representation_ifc_parameters(
|
||||
geometry: type[tool.Geometry], obj: bpy.types.Object, should_sync_changes_first: bool = False
|
||||
) -> None:
|
||||
def get_representation_ifc_parameters(geometry: type[tool.Geometry], obj: bpy.types.Object) -> None:
|
||||
geometry.import_representation_parameters(geometry.get_object_data(obj))
|
||||
|
||||
|
||||
|
||||
@@ -72,6 +72,10 @@ def assign_class(
|
||||
"""
|
||||
Args:
|
||||
context: is not optional if `should_add_representation` is True
|
||||
|
||||
TODO: Do NOT use should_add_representation. Because it internally calls
|
||||
geometry.add_representation which is 1,000 lines of Blender -> IFC magic.
|
||||
Instead, explicitly create a representation using tool.Geometry.
|
||||
"""
|
||||
if ifc.get_entity(obj):
|
||||
return
|
||||
|
||||
@@ -394,7 +394,7 @@ class Drawing:
|
||||
def sync_object_placement(cls, obj): pass
|
||||
def synchronise_ifc_and_text_attributes(cls, obj): pass
|
||||
def update_embedded_svg_location(cls, uri, old_location, new_location): pass
|
||||
def update_newline_at_and_symbol(cls, obj): pass
|
||||
def update_text_annotation_properties(cls, obj): pass
|
||||
def update_text_size_pset(cls, obj): pass
|
||||
|
||||
|
||||
@@ -1092,7 +1092,7 @@ class Type:
|
||||
def get_type_occurrences(cls, element_type): pass
|
||||
def has_material_usage(cls, element): pass
|
||||
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
|
||||
def run_geometry_switch_representation(cls, obj=None, representation=None, should_reload=None, is_global=None): pass
|
||||
def run_geometry_switch_representation(cls, obj=None, representation=None): pass
|
||||
|
||||
|
||||
@interface
|
||||
|
||||
@@ -477,6 +477,8 @@ class Blender(bonsai.core.tool.Blender):
|
||||
def ensure_bin_in_path(cls) -> None:
|
||||
"""Check 'bin' folder is in PATH, if not add for this session"""
|
||||
bin_dir = str(Path(__file__).parent.parent.resolve() / "libs" / "bin")
|
||||
if not os.path.isdir(bin_dir):
|
||||
return # Maybe the user is using a system-wide Python package. See #7157.
|
||||
current_path = os.environ["PATH"]
|
||||
if bin_dir not in current_path:
|
||||
os.environ["PATH"] = current_path + os.pathsep + bin_dir
|
||||
@@ -1504,28 +1506,21 @@ class Blender(bonsai.core.tool.Blender):
|
||||
return bpy.context.preferences.addons[blender_package_name].preferences
|
||||
|
||||
@classmethod
|
||||
def get_sun_position_addon(cls) -> Union[types.ModuleType, None]:
|
||||
# Check if it's installed as legacy Blender addon.
|
||||
def get_addon(cls, name: str) -> Union[types.ModuleType, None]:
|
||||
import importlib
|
||||
|
||||
try:
|
||||
sun_position = importlib.import_module("sun_position")
|
||||
return importlib.import_module(name) # Legacy Blender addon
|
||||
except ImportError:
|
||||
sun_position = None
|
||||
|
||||
if sun_position:
|
||||
return sun_position
|
||||
pass
|
||||
|
||||
for package_name in bpy.context.preferences.addons.keys():
|
||||
if package_name.endswith(".sun_position"):
|
||||
if package_name.endswith(f".{name}"):
|
||||
try:
|
||||
sun_position = importlib.import_module(package_name)
|
||||
return sun_position
|
||||
return importlib.import_module(package_name)
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
return sun_position
|
||||
|
||||
@classmethod
|
||||
def get_sun_props(cls) -> Union[SunPosProperties, None]:
|
||||
assert (scene := bpy.context.scene)
|
||||
@@ -1859,6 +1854,93 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
return unit_scale
|
||||
|
||||
@classmethod
|
||||
def reset_object_visibility(cls):
|
||||
override = cls.get_viewport_context()
|
||||
with bpy.context.temp_override(**override):
|
||||
bpy.ops.object.hide_view_clear(select=False)
|
||||
|
||||
@classmethod
|
||||
def isolate_objects(cls, objs):
|
||||
previously_selected = {o.name for o in bpy.context.selected_objects}
|
||||
previously_active = bpy.context.view_layer.objects.active
|
||||
|
||||
override = cls.get_viewport_context()
|
||||
with bpy.context.temp_override(**override):
|
||||
bpy.ops.object.hide_view_clear(select=False)
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in objs:
|
||||
obj.select_set(True)
|
||||
with bpy.context.temp_override(**override):
|
||||
bpy.ops.object.hide_view_set(unselected=True)
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for name in previously_selected:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = previously_active
|
||||
|
||||
@classmethod
|
||||
def sync_render_visibility(cls):
|
||||
# Doing bpy.ops.object.hide_render_clear_all() or
|
||||
# bpy.ops.object.isolate_type_render() is extremely slow.
|
||||
# Hopefully this doesn't crash on Windows, it doesn't crash on Linux.
|
||||
should_hides = [0 if obj.visible_get() else 1 for obj in bpy.data.objects]
|
||||
should_hides = np.fromiter(should_hides, dtype=np.uint8, count=len(should_hides))
|
||||
bpy.data.objects.foreach_set("hide_render", should_hides)
|
||||
return # Otherwise...
|
||||
# for obj in bpy.data.objects:
|
||||
# if not obj.data:
|
||||
# continue
|
||||
# # For speed, check equality prior to change to prevent needless updates
|
||||
# if (is_visible := obj.visible_get()) and obj.hide_render is True:
|
||||
# obj.hide_render = False
|
||||
# elif not is_visible and obj.hide_render is False:
|
||||
# obj.hide_render = True
|
||||
|
||||
@classmethod
|
||||
def hide_objects(cls, objs):
|
||||
previously_selected = {o.name for o in bpy.context.selected_objects}
|
||||
previously_active = bpy.context.view_layer.objects.active
|
||||
|
||||
override = cls.get_viewport_context()
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in objs:
|
||||
obj.select_set(True)
|
||||
with bpy.context.temp_override(**override):
|
||||
bpy.ops.object.hide_view_set(unselected=False)
|
||||
|
||||
for name in previously_selected:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = previously_active
|
||||
|
||||
@classmethod
|
||||
def show_objects(cls, objs):
|
||||
previously_selected = {o.name for o in bpy.context.selected_objects}
|
||||
previously_active = bpy.context.view_layer.objects.active
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
override = cls.get_viewport_context()
|
||||
with bpy.context.temp_override(**override):
|
||||
bpy.ops.object.hide_view_clear(select=True)
|
||||
|
||||
for obj in bpy.context.selected_objects:
|
||||
if obj in objs:
|
||||
obj.select_set(False)
|
||||
with bpy.context.temp_override(**override):
|
||||
bpy.ops.object.hide_view_set(unselected=False)
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for name in previously_selected:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = previously_active
|
||||
|
||||
@classmethod
|
||||
def validate_shader_batch_data(cls, pos: Any, indices: Optional[Any]) -> bool:
|
||||
"""Validate shader batch data.
|
||||
|
||||
@@ -51,9 +51,13 @@ class Collector(bonsai.core.tool.Collector):
|
||||
if tool.Geometry.is_locked(element):
|
||||
tool.Geometry.lock_object(obj)
|
||||
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
||||
if not tool.Spatial.get_grid_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif element.is_a("IfcGrid"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
tool.Geometry.lock_object(obj)
|
||||
if not tool.Spatial.get_grid_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
|
||||
if element.is_a("IfcProject"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
@@ -65,8 +69,12 @@ class Collector(bonsai.core.tool.Collector):
|
||||
collection = cls._create_project_child_collection("IfcTypeProduct")
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
elif element.is_a("IfcSpace"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
tool.Geometry.lock_object(obj)
|
||||
collection = cls._create_project_child_collection("IfcSpace")
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
if not tool.Spatial.get_spatial_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif element.is_a("IfcStructuralItem"):
|
||||
collection = cls._create_project_child_collection("IfcStructuralItem")
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
@@ -90,6 +98,8 @@ class Collector(bonsai.core.tool.Collector):
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
||||
if not tool.Spatial.get_spatial_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif (
|
||||
tool.Ifc.get_schema() != "IFC2X3"
|
||||
and element.is_a("IfcSpatialElement")
|
||||
@@ -101,6 +111,8 @@ class Collector(bonsai.core.tool.Collector):
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
||||
if not tool.Spatial.get_spatial_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
if collection := cls._create_own_collection(obj):
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
|
||||
@@ -273,18 +273,16 @@ class Cost(bonsai.core.tool.Cost):
|
||||
return
|
||||
props = cls.get_cost_props()
|
||||
props.cost_item_type_products.clear()
|
||||
# TODO implement process and resource types
|
||||
# props.cost_item_processes.clear()
|
||||
# props.cost_item_resources.clear()
|
||||
props.cost_item_processes.clear()
|
||||
props.cost_item_resources.clear()
|
||||
for rel in cost_item.Controls or []:
|
||||
for related_object in rel.RelatedObjects:
|
||||
if related_object.is_a("IfcTypeProduct"):
|
||||
new = props.cost_item_type_products.add()
|
||||
# TODO implement process and resource types
|
||||
# elif related_object.is_a("IfcProcess"):
|
||||
# new = props.cost_item_processes.add()
|
||||
# elif related_object.is_a("IfcResource"):
|
||||
# new = props.cost_item_resources.add()
|
||||
elif related_object.is_a("IfcProcess"):
|
||||
new = props.cost_item_processes.add()
|
||||
elif related_object.is_a("IfcResource"):
|
||||
new = props.cost_item_resources.add()
|
||||
new.ifc_definition_id = related_object.id()
|
||||
new.name = related_object.Name or "Unnamed"
|
||||
|
||||
|
||||
+104
-17
@@ -18,6 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import bmesh
|
||||
import bpy
|
||||
@@ -33,7 +34,7 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from mathutils import Vector
|
||||
from collections import defaultdict
|
||||
from typing import Literal, TYPE_CHECKING, assert_never
|
||||
from typing import Literal, TYPE_CHECKING, assert_never, Union
|
||||
from collections.abc import Iterable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -136,12 +137,30 @@ class Debug(bonsai.core.tool.Debug):
|
||||
"PERSON",
|
||||
"PERSON_AND_ORGANIZATION",
|
||||
],
|
||||
by_name_or_identification_only: bool = False,
|
||||
) -> dict[str, list[str]]:
|
||||
"""Merge identical objects.
|
||||
|
||||
Note that Styles UI (or other UI) should be updated manually after using this method.
|
||||
|
||||
Args:
|
||||
object_type: The type of object to merge
|
||||
by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
|
||||
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
|
||||
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
|
||||
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
|
||||
"""
|
||||
|
||||
def normalize_name(name: str) -> str:
|
||||
"""Remove .XXX suffix pattern from names (e.g., 'foo.001' -> 'foo')"""
|
||||
if not name:
|
||||
return ""
|
||||
# Match pattern: name ending with .digits
|
||||
match = re.match(r"^(.+)\.\d+$", name)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return name
|
||||
|
||||
def get_hash(element: ifcopenshell.entity_instance) -> int:
|
||||
data = element.get_info_2(include_identifier=False, recursive=True)
|
||||
if object_type == "APPLICATION":
|
||||
@@ -152,6 +171,49 @@ class Debug(bonsai.core.tool.Debug):
|
||||
data["TheOrganization"] = element.TheOrganization.id()
|
||||
return hash(json.dumps(data, sort_keys=True))
|
||||
|
||||
def get_name_key(element: ifcopenshell.entity_instance) -> str:
|
||||
"""Get key based on name/identifier attribute for the given object type"""
|
||||
if object_type == "STYLE":
|
||||
name = element.Name if element.Name else ""
|
||||
return normalize_name(name)
|
||||
elif object_type == "MATERIAL":
|
||||
name = element.Name if element.Name else ""
|
||||
return normalize_name(name)
|
||||
elif object_type == "ORGANIZATION":
|
||||
name = element.Name if element.Name else ""
|
||||
return normalize_name(name)
|
||||
elif object_type == "APPLICATION":
|
||||
name = element.ApplicationFullName if element.ApplicationFullName else ""
|
||||
return normalize_name(name)
|
||||
elif object_type == "PERSON":
|
||||
ident = element.Identification if element.Identification else ""
|
||||
return normalize_name(ident)
|
||||
elif object_type == "PERSON_AND_ORGANIZATION":
|
||||
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
|
||||
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
|
||||
return f"{normalize_name(person_id)}|{normalize_name(org_name)}"
|
||||
else:
|
||||
assert_never(object_type)
|
||||
|
||||
def get_element_name(element: ifcopenshell.entity_instance) -> str:
|
||||
"""Get the actual name/identifier from element for sorting purposes"""
|
||||
if object_type == "STYLE":
|
||||
return element.Name if element.Name else ""
|
||||
elif object_type == "MATERIAL":
|
||||
return element.Name if element.Name else ""
|
||||
elif object_type == "ORGANIZATION":
|
||||
return element.Name if element.Name else ""
|
||||
elif object_type == "APPLICATION":
|
||||
return element.ApplicationFullName if element.ApplicationFullName else ""
|
||||
elif object_type == "PERSON":
|
||||
return element.Identification if element.Identification else ""
|
||||
elif object_type == "PERSON_AND_ORGANIZATION":
|
||||
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
|
||||
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
|
||||
return f"{person_id}|{org_name}"
|
||||
else:
|
||||
assert_never(object_type)
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
merged_element_types: dict[str, list[str]] = {}
|
||||
|
||||
@@ -179,22 +241,47 @@ class Debug(bonsai.core.tool.Debug):
|
||||
for element_type in element_types:
|
||||
elements = ifc_file.by_type(element_type, include_subtypes=False)
|
||||
|
||||
# Calculate hashes.
|
||||
hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list)
|
||||
for element in elements:
|
||||
# Except for styles, ignore unnamed elements as they may be not safe to merge
|
||||
merge_optional_names = ("STYLE", "PERSON")
|
||||
not_optional_name = ("APPLICATION", "ORGANIZATION")
|
||||
has_no_name = ("PERSON_AND_ORGANIZATION",)
|
||||
if (
|
||||
object_type not in merge_optional_names
|
||||
and object_type not in not_optional_name
|
||||
and object_type not in has_no_name
|
||||
and not element.Name
|
||||
):
|
||||
continue
|
||||
element_hash = get_hash(element)
|
||||
hash_to_elements[element_hash].append(element)
|
||||
# Calculate hashes or name keys.
|
||||
hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
|
||||
|
||||
if by_name_or_identification_only:
|
||||
# Group by name/identifier only (with .XXX suffix normalization)
|
||||
hash_to_elements = defaultdict(list)
|
||||
for element in elements:
|
||||
name_key = get_name_key(element)
|
||||
# Skip elements without a valid identifier
|
||||
if not name_key:
|
||||
continue
|
||||
hash_to_elements[name_key].append(element)
|
||||
|
||||
# Sort elements within each group to keep the one without suffix (or lowest suffix)
|
||||
for name_key in hash_to_elements:
|
||||
# Sort by: 1) prefer names without .XXX suffix, 2) then by original name
|
||||
def sort_key(el):
|
||||
name = get_element_name(el)
|
||||
# Check if name has .XXX suffix
|
||||
has_suffix = bool(re.match(r"^.+\.\d+$", name))
|
||||
# Return tuple: (has_suffix, name) - sort by no suffix first, then alphabetically
|
||||
return (has_suffix, name)
|
||||
|
||||
hash_to_elements[name_key].sort(key=sort_key)
|
||||
else:
|
||||
# Group by full hash
|
||||
hash_to_elements = defaultdict(list)
|
||||
for element in elements:
|
||||
# Except for styles, ignore unnamed elements as they may be not safe to merge
|
||||
merge_optional_names = ("STYLE", "PERSON")
|
||||
not_optional_name = ("APPLICATION", "ORGANIZATION")
|
||||
has_no_name = ("PERSON_AND_ORGANIZATION",)
|
||||
if (
|
||||
object_type not in merge_optional_names
|
||||
and object_type not in not_optional_name
|
||||
and object_type not in has_no_name
|
||||
and not element.Name
|
||||
):
|
||||
continue
|
||||
element_hash = get_hash(element)
|
||||
hash_to_elements[element_hash].append(element)
|
||||
|
||||
merged_elements_names: list[str] = []
|
||||
# Merge elements.
|
||||
|
||||
+535
-320
File diff suppressed because it is too large
Load Diff
@@ -233,20 +233,20 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
||||
elif element.is_a("IfcAnnotation"):
|
||||
if element.ObjectType == "DRAWING":
|
||||
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
||||
elif tool.Drawing.is_auto_annotation(element):
|
||||
return # For now, these are special referenced objects and cannot be deleted. Exclude instead.
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element)
|
||||
tool.Boundary.undecorate_boundary(obj)
|
||||
return bpy.data.objects.remove(obj)
|
||||
elif element.is_a("IfcGridAxis"):
|
||||
is_last_axis = False
|
||||
# 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
|
||||
):
|
||||
is_last_axis = True
|
||||
if is_last_axis:
|
||||
return
|
||||
ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element)
|
||||
return bpy.data.objects.remove(obj)
|
||||
@@ -1350,9 +1350,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
@@ -1388,9 +1385,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_representation,
|
||||
should_reload=False,
|
||||
should_sync_changes_first=False,
|
||||
is_global=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -248,7 +248,7 @@ class Ifc(bonsai.core.tool.Ifc):
|
||||
|
||||
Marking object as edited is an optimization mechanism - instead of saving
|
||||
changed geometry to IFC, we mark it as changed and then it's saved later
|
||||
(typically during project save or switch_representation(should_sync_changes_first=True)).
|
||||
(typically during project save).
|
||||
|
||||
Other caveat of using edited objects is that it won't have an effect for objects with openings,
|
||||
since we can't deduce non-openings representation from edited representation with openings.
|
||||
|
||||
@@ -786,17 +786,21 @@ class Model(bonsai.core.tool.Model):
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
axis = ifcopenshell.util.element.get_pset(element, "EPset_Parametric", "LayerSetDirection")
|
||||
if axis is None:
|
||||
if element.is_a() in [
|
||||
if element.is_a() in (
|
||||
"IfcSlabType",
|
||||
"IfcRoofType",
|
||||
"IfcRampType",
|
||||
"IfcPlateType",
|
||||
"IfcCovering",
|
||||
"IfcFurniture",
|
||||
]:
|
||||
"IfcSlab",
|
||||
"IfcRoof",
|
||||
"IfcRamp",
|
||||
"IfcPlate",
|
||||
):
|
||||
axis = "AXIS3"
|
||||
else:
|
||||
elif element.is_a() in ("IfcWallType", "IfcWall"):
|
||||
axis = "AXIS2"
|
||||
else:
|
||||
return
|
||||
return f"LAYER{axis[-1]}"
|
||||
elif material.is_a("IfcMaterialProfileSetUsage"):
|
||||
# TODO: remove after we support editing profile usages with IfcRevolvedAreaSolid.
|
||||
@@ -1091,9 +1095,6 @@ class Model(bonsai.core.tool.Model):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -1306,9 +1307,6 @@ class Model(bonsai.core.tool.Model):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -1405,7 +1403,6 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
number_of_risers = number_of_treads + 1
|
||||
tread_rise = height / number_of_risers
|
||||
custom_tread_run = any(run != 0 for run in custom_first_last_tread_run)
|
||||
nosing_overlap = max(nosing_length, 0)
|
||||
nosing_tread_gap = -min(nosing_length, 0)
|
||||
nosing_overlap_offset = -V_(nosing_overlap, 0)
|
||||
@@ -1436,19 +1433,26 @@ class Model(bonsai.core.tool.Model):
|
||||
default_tread_offset = Vector([tread_run, tread_rise])
|
||||
|
||||
def get_tread_data(i):
|
||||
if custom_tread_run:
|
||||
current_tread_run = None
|
||||
if i == 0:
|
||||
current_tread_run = custom_first_last_tread_run[0]
|
||||
elif i == number_of_risers - 1:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
# Check if this is first or last tread with custom run
|
||||
current_tread_run = None
|
||||
if i == 0 and custom_first_last_tread_run[0] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[0]
|
||||
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
|
||||
if current_tread_run is not None:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run
|
||||
|
||||
# Handle zero-width treads
|
||||
if current_tread_run == 0:
|
||||
# For zero width, just return vertical offset with no horizontal tread
|
||||
return tread_offset, ()
|
||||
|
||||
tread_verts = deepcopy(default_tread_verts)
|
||||
tread_verts[-1].x = current_tread_run
|
||||
return tread_offset, tread_verts
|
||||
|
||||
if current_tread_run:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run
|
||||
tread_verts = deepcopy(default_tread_verts)
|
||||
tread_verts[-1].x = current_tread_run
|
||||
return tread_offset, tread_verts
|
||||
return default_tread_offset, default_tread_verts
|
||||
|
||||
# treads
|
||||
@@ -1456,9 +1460,13 @@ class Model(bonsai.core.tool.Model):
|
||||
for i in range(number_of_risers):
|
||||
last_vert_i = len(vertices) - 1
|
||||
tread_offset, tread_verts = get_tread_data(i)
|
||||
current_tread_verts = [v + current_offset for v in tread_verts]
|
||||
edges.extend(default_tread_edges + last_vert_i)
|
||||
vertices.extend(current_tread_verts)
|
||||
|
||||
# Skip adding vertices/edges for zero-width treads
|
||||
if tread_verts:
|
||||
current_tread_verts = [v + current_offset for v in tread_verts]
|
||||
edges.extend(default_tread_edges + last_vert_i)
|
||||
vertices.extend(current_tread_verts)
|
||||
|
||||
current_offset += tread_offset
|
||||
|
||||
if stair_type == "WOOD/STEEL":
|
||||
@@ -1473,35 +1481,47 @@ class Model(bonsai.core.tool.Model):
|
||||
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
|
||||
|
||||
def get_tread_data(i):
|
||||
if custom_tread_run:
|
||||
current_tread_run = None
|
||||
if i == 0 and custom_first_last_tread_run[0] != 0:
|
||||
current_tread_run = custom_first_last_tread_run[0]
|
||||
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 0:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
# Check if this is first or last tread with custom run
|
||||
current_tread_run = None
|
||||
if i == 0 and custom_first_last_tread_run[0] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[0]
|
||||
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
|
||||
if current_tread_run is not None:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run + nosing_tread_gap
|
||||
|
||||
# Handle zero-width treads
|
||||
if current_tread_run == 0:
|
||||
return tread_offset, ()
|
||||
|
||||
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
|
||||
return tread_offset, tread_verts
|
||||
|
||||
if current_tread_run:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run + nosing_tread_gap
|
||||
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
|
||||
return tread_offset, tread_verts
|
||||
return default_tread_offset, default_tread_verts
|
||||
|
||||
# each tread is a separate shape
|
||||
cur_offset = V_(0, 0)
|
||||
tread_index = 0
|
||||
for i in range(number_of_risers):
|
||||
tread_offset, tread_verts = get_tread_data(i)
|
||||
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
|
||||
vertices.extend(cur_trade_shape)
|
||||
|
||||
cur_vertex = i * 4
|
||||
verts_to_add = (
|
||||
(cur_vertex, cur_vertex + 1),
|
||||
(cur_vertex + 1, cur_vertex + 2),
|
||||
(cur_vertex + 2, cur_vertex + 3),
|
||||
(cur_vertex + 3, cur_vertex),
|
||||
)
|
||||
edges.extend(verts_to_add)
|
||||
# Skip adding vertices/edges for zero-width treads
|
||||
if tread_verts:
|
||||
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
|
||||
vertices.extend(cur_trade_shape)
|
||||
|
||||
cur_vertex = tread_index * 4
|
||||
verts_to_add = (
|
||||
(cur_vertex, cur_vertex + 1),
|
||||
(cur_vertex + 1, cur_vertex + 2),
|
||||
(cur_vertex + 2, cur_vertex + 3),
|
||||
(cur_vertex + 3, cur_vertex),
|
||||
)
|
||||
edges.extend(verts_to_add)
|
||||
tread_index += 1
|
||||
|
||||
cur_offset += tread_offset
|
||||
|
||||
elif stair_type == "GENERIC":
|
||||
@@ -1652,9 +1672,6 @@ class Model(bonsai.core.tool.Model):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
@@ -2468,9 +2485,6 @@ class Model(bonsai.core.tool.Model):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=rep,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
|
||||
@@ -155,7 +155,6 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
@classmethod
|
||||
def point_is_visible_in_clipping_plane(cls, vertex):
|
||||
normals = tool.Project.get_clipping_planes_normals()
|
||||
print("NORMALS", normals)
|
||||
if not normals:
|
||||
return True
|
||||
for normal in normals:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user