mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
336 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7864657f88 | |||
| 8d617c87c8 | |||
| 1b2ad6c93f | |||
| ffe9f65b3c | |||
| 5a8f557e7f | |||
| 5b56093147 | |||
| bcb63f450b | |||
| e4c82e2c92 | |||
| 8fb10d2ff3 | |||
| 37d29e5d47 | |||
| 7221e72cfa | |||
| c828c1b08d | |||
| fcf5614004 | |||
| 5a4d60e08c | |||
| a675365dfd | |||
| 5ab6deb121 | |||
| cbd26896ba | |||
| ceeca752ea | |||
| 9d437b5bc3 | |||
| 1d0b4675c0 | |||
| 497ebc55e7 | |||
| 97a501036b | |||
| 592c51b526 | |||
| 3dd7290ee1 | |||
| d7ac5f2441 | |||
| 461404cd45 | |||
| bfd5b1bede | |||
| 0d0b6cc6b2 | |||
| 57bc709b6f | |||
| b7b6e4aadf | |||
| 9c39ac0a48 | |||
| a77f1b9fd1 | |||
| 63921e3fd8 | |||
| 27d0b04db5 | |||
| 87283874a1 | |||
| 584d37de50 | |||
| ca2058d56b | |||
| 481676e51b | |||
| fe502577c9 | |||
| 65e8ca17d2 | |||
| 6bd7f02208 | |||
| 452d0f6023 | |||
| 07005a9d3b | |||
| 450ce689ba | |||
| 7c210721e6 | |||
| 7e48d712a2 | |||
| 9eedbc6f72 | |||
| 68b021b0e6 | |||
| a559b72e18 | |||
| 32496c40b3 | |||
| 1b249b1013 | |||
| d3fcf39c76 | |||
| 692ef5ecea | |||
| f56a97512a | |||
| 4f5ebef5da | |||
| 57a724e80f | |||
| 1b8652a789 | |||
| c1aa702896 | |||
| 5b43d2b75e | |||
| 3330e79556 | |||
| 88d09c83b7 | |||
| 5e6b96ee99 | |||
| bd07078def | |||
| 1d01d1f184 | |||
| 3cee0811d4 | |||
| a46228a57e | |||
| 158fe92644 | |||
| 12166ddbba | |||
| 36a0c22217 | |||
| 59cc9d4a23 | |||
| f3d918f76c | |||
| 613015e37e | |||
| be0eea1bed | |||
| 37bbc0b693 | |||
| 30093499fa | |||
| 8e95bf73f1 | |||
| 05f7aac853 | |||
| b135dd6994 | |||
| a5e7d7c4cc | |||
| 9680f6b29c | |||
| bc8dd3a3ab | |||
| 9caf47b41f | |||
| 5e1787b146 | |||
| 07147f4b7f | |||
| 4dfe15bd88 | |||
| 5896630630 | |||
| 48b1493c5c | |||
| ee75ea1182 | |||
| 1876a788a9 | |||
| 9d28628157 | |||
| 9b257b0a10 | |||
| db7900027e | |||
| 2af7470950 | |||
| e1c3c44fb3 | |||
| e7c422469a | |||
| 52d3bae543 | |||
| b130925c5f | |||
| 890665b77b | |||
| 99c95bc92d | |||
| 8647f9c357 | |||
| 300855276f | |||
| 1a8831012b | |||
| 1bc4e82b7d | |||
| fc197c1d32 | |||
| 33cfa9e2b1 | |||
| 9e9d664511 | |||
| 1b9acadd81 | |||
| c5f6d47c44 | |||
| d19b6d72ef | |||
| 919231b096 | |||
| cead5cc75d | |||
| 9396cb226f | |||
| 56a63cab6e | |||
| 449d13bd5f | |||
| 487a4f345d | |||
| d6daa64c48 | |||
| bf0aa247a0 | |||
| a7c8c908fe | |||
| 64686dde5c | |||
| 29ab446f8c | |||
| 51e2e7cd82 | |||
| e879dac4bd | |||
| 145c629d92 | |||
| 3fe9279bb4 | |||
| 428d88f01c | |||
| 3986d4e85b | |||
| 02d35b6047 | |||
| 3cc5931eae | |||
| db5ec77730 | |||
| bec46562f5 | |||
| 1508c4ab05 | |||
| 99ae768ddf | |||
| fb902a04f5 | |||
| e46a99870b | |||
| 3fbba405d4 | |||
| 989415f423 | |||
| 7a3c7704b5 | |||
| 5d7407e2b9 | |||
| 4a5f1b01a1 | |||
| a038fa62ba | |||
| bb1127d815 | |||
| 40cf322fb4 | |||
| f32ccec154 | |||
| dc0b73f46f | |||
| 36cb625c4e | |||
| 89314733ff | |||
| d4e945222d | |||
| fe80620a5d | |||
| 166ddb4799 | |||
| 8e2a310f92 | |||
| 339a0606c0 | |||
| b6f1119eaa | |||
| 139a024d5f | |||
| 9491096485 | |||
| a34e42a519 | |||
| 6a7ecf7f58 | |||
| 96da78c670 | |||
| 9adc8e484a | |||
| e8eb5e4d0a | |||
| 080222ffe6 | |||
| abef655482 | |||
| 85b3f831dc | |||
| d8ece32d62 | |||
| 0df7117d37 | |||
| 91d67e6371 | |||
| 87d1b0637a | |||
| b8adddc648 | |||
| 25d3edc537 | |||
| 7f8cb80281 | |||
| a2ce2ea23a | |||
| 7f7aef0e6e | |||
| 821b5380ea | |||
| e8bcbc83a4 | |||
| 091af3c1c9 | |||
| 4cc50301dd | |||
| f643a1615e | |||
| 71e78c67f8 | |||
| e7c4a19d9e | |||
| 6c31d43e72 | |||
| 83b56d591b | |||
| e5b26067ab | |||
| a4fae94be8 | |||
| af4851f03a | |||
| 155d4c44ae | |||
| 48119baf97 | |||
| dd1989aebb | |||
| 0806430700 | |||
| 2d97f6638b | |||
| b10e5ad4d6 | |||
| 24f991ceb1 | |||
| ac11baa0c4 | |||
| 1609a77b77 | |||
| dd2f091a52 | |||
| a7bf0e7d5e | |||
| 19bdd3e622 | |||
| e1bf891ec6 | |||
| 4009834b97 | |||
| 77f5e1fbad | |||
| c4eb77d544 | |||
| 8a4ea43239 | |||
| f056e22f60 | |||
| 05e81040ce | |||
| 8d8133e780 | |||
| dceb35782e | |||
| 2c5f0c6cf2 | |||
| abd667684d | |||
| 31b5f88497 | |||
| 77a7026d33 | |||
| 9c5f3f9754 | |||
| 5d56025014 | |||
| ad62c75f4f | |||
| 7a5bd03216 | |||
| 593e0e28a9 | |||
| ed2b2de79a | |||
| 5a511eecc1 | |||
| 83e0fa1287 | |||
| a2dbdbf8cc | |||
| 3d68bbc0ca | |||
| 40f9f261ae | |||
| c08cc58904 | |||
| ebdb122a22 | |||
| 5de2752685 | |||
| d2245a95c7 | |||
| 47075e95bd | |||
| 9d4c980e02 | |||
| 53c1a1537d | |||
| 7db2f66685 | |||
| 6dd34b1fa4 | |||
| 862713cda4 | |||
| 06c1668e42 | |||
| 5f9f0f26eb | |||
| 8db22cdaf7 | |||
| 3bed9262e5 | |||
| 0c171a457b | |||
| b5e8dfd863 | |||
| c9df87fc25 | |||
| 30925a4b3c | |||
| ccf0efffce | |||
| 169c4392a9 | |||
| e543785959 | |||
| 3617d119cb | |||
| 5d1f28c3cb | |||
| 6f04eb12d0 | |||
| b46136e9f8 | |||
| c3fe2d0ee3 | |||
| aeed371888 | |||
| a50c8869a5 | |||
| 04d1fe4052 | |||
| 4e1d0bcf22 | |||
| d2126e5929 | |||
| 3bdfbebc02 | |||
| 1a9bd1891e | |||
| 1599bc14b3 | |||
| 30b74ab09c | |||
| 24bee01f01 | |||
| 7329c485ea | |||
| 9c93b12084 | |||
| b97c7d39ad | |||
| fa0c1c5d2a | |||
| e4c49ce57b | |||
| 3ae77c68e8 | |||
| 8b1cb8e7ae | |||
| ce0348518d | |||
| a660027b93 | |||
| 44ef409335 | |||
| 089edff1db | |||
| bd5a1b5bda | |||
| ba3517f3aa | |||
| 7f144b7b95 | |||
| 19ac0b6c38 | |||
| 3562437293 | |||
| e8ece71d22 | |||
| 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 |
@@ -0,0 +1,8 @@
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
@@ -34,11 +34,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
|
||||
@@ -72,6 +67,10 @@ 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
|
||||
@@ -82,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: build-logs-osx-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -140,7 +139,7 @@ jobs:
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -9,41 +9,75 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
path: IfcOpenShell
|
||||
|
||||
- name: Checkout Pyodide
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v5
|
||||
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@v5
|
||||
with:
|
||||
name: build-logs-pyodide
|
||||
path: |
|
||||
ifcopenshell_build/*/*/logs/*.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Run wheel tests
|
||||
run: |
|
||||
cp -r IfcOpenShell/pyodide/test test
|
||||
# venv set up in build_pyodide.sh.
|
||||
source .venv/bin/activate
|
||||
uv pip install pytest-pyodide
|
||||
PYODIDE_ROOT_DIST=`pyodide config get pyodide_root`/dist
|
||||
# `pytest-pyodide` requires pyodide in 'pyodide' directory in cwd, when running `pytest`.
|
||||
cp -r $PYODIDE_ROOT_DIST test/pyodide
|
||||
cp dist/ifcopenshell-*.whl test/pyodide
|
||||
cd test
|
||||
pytest --capture=no
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/pyodide/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
git config user.name "IfcOpenBot"
|
||||
git config user.email "ifcopenbot@ifcopenshell.org"
|
||||
git add */*/install/cache-*.tar.gz
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -29,12 +29,12 @@ jobs:
|
||||
aws --version
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
# Using fork to support Rocky.
|
||||
uses: Andrej730/ccache-action@main
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: build-logs-rocky
|
||||
path: |
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -29,12 +29,12 @@ jobs:
|
||||
aws --version
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
# Using fork to support Rocky.
|
||||
uses: Andrej730/ccache-action@main
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: build-logs-rocky-arm64
|
||||
path: |
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -9,16 +9,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ['3.9.11', '3.10.3', '3.11.8', '3.12.1', '3.13.0']
|
||||
arch: ['x64']
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: _deps-vs2022-x64-installed
|
||||
@@ -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
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: win-${{ matrix.arch }}
|
||||
# Windows ccache needs ~1GB
|
||||
# and with default 500MB some cache gets deleted, leading to misses.
|
||||
max-size: 5000MB
|
||||
|
||||
- name: Run Build Script And Pack .zip Archives
|
||||
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,37 +72,8 @@ 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
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -19,8 +19,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # https://github.com/actions/checkout
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5 # https://github.com/actions/checkout
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -9,15 +9,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Action - checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -54,8 +54,8 @@ jobs:
|
||||
short_name: macosm1,
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout bonsai_unstable_repo repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/bonsai_unstable_repo
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
|
||||
@@ -43,8 +43,8 @@ jobs:
|
||||
short_name: macosm1,
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -37,8 +37,8 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
environment-name: test-env
|
||||
create-args: >-
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
date: ${{ steps.date.outputs.date }}
|
||||
verdate: ${{ steps.verdate.outputs.verdate }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
make package
|
||||
working-directory: build
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
@@ -89,31 +89,31 @@ jobs:
|
||||
name: Docker Build, Tag, Push
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v6.0.0
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
path: artifacts/
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to Dockerhub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: aecgeeks
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
-
|
||||
name: Build container image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: artifacts
|
||||
repository: aecgeeks/ifcopenshell
|
||||
|
||||
@@ -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",
|
||||
@@ -34,6 +34,10 @@ jobs:
|
||||
name: "Linux 64bit",
|
||||
short_name: linux64
|
||||
}
|
||||
- {
|
||||
name: "Linux ARM 64bit",
|
||||
short_name: linuxarm64
|
||||
}
|
||||
- {
|
||||
name: "MacOS Intel 64bit",
|
||||
short_name: macos64
|
||||
@@ -43,10 +47,10 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -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",
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -25,8 +25,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -19,8 +19,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -10,9 +10,9 @@ jobs:
|
||||
publish_website:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
- name: Checkout ifctester_org_static_html
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/ifctester_org_static_html
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -35,12 +35,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
@@ -64,17 +64,22 @@ jobs:
|
||||
libboost-regex-dev \
|
||||
libboost-system-dev \
|
||||
libboost-thread-dev \
|
||||
swig libpcre3-dev libxml2-dev \
|
||||
libpcre3-dev libxml2-dev \
|
||||
libtbb-dev nlohmann-json3-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libhdf5-dev libcgal-dev libeigen3-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
# TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released
|
||||
# to update hardcoded references to commits in some other workflows.
|
||||
# Then we can switch back to 1.2 in all actions.
|
||||
uses: hendrikmuhs/ccache-action@v1.2.19
|
||||
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
|
||||
@@ -95,6 +100,41 @@ jobs:
|
||||
..
|
||||
sudo make -j$(nproc) install
|
||||
|
||||
# OpenCOLLADA is ancient, but we still have it in the main build.
|
||||
# So adding it to CI to catch any breakages.
|
||||
- name: build OpenCOLLADA
|
||||
run: |
|
||||
git clone https://github.com/KhronosGroup/OpenCOLLADA
|
||||
cd OpenCOLLADA
|
||||
git checkout v1.6.68
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/pr622_and_disable_subdirs.patch
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/allow_static_libraries_config_on_unix.patch
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_FLAGS_INIT="-fPIC"
|
||||
sudo make -j$(nproc) install
|
||||
|
||||
# Ubuntu has `swig` package, but we build it to match the version we use in the main build.
|
||||
# To avoid failing tests when checking stub generation.
|
||||
- name: build swig
|
||||
run: |
|
||||
# Remove default swig to avoid conflicts.
|
||||
sudo apt remove --purge swig swig4.0
|
||||
sudo apt-get install -y libpcre2-dev bison
|
||||
git clone https://github.com/swig/swig
|
||||
cd swig
|
||||
git checkout v4.1.0
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
sudo make -j$(nproc) install
|
||||
|
||||
- name: Build ifcopenshell
|
||||
run: |
|
||||
echo $Python3_ROOT_DIR
|
||||
@@ -112,8 +152,6 @@ 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 \
|
||||
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (recursive)
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: src/pyodide/demo-app/
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install C++ dependencies
|
||||
|
||||
+2
-5
@@ -17,6 +17,7 @@
|
||||
# General Python residue
|
||||
__pycache__
|
||||
*.py.bak
|
||||
venv
|
||||
|
||||
# Visual Studio Code files
|
||||
.vscode
|
||||
@@ -78,8 +79,6 @@ src/bonsai/bonsai/translations.py
|
||||
|
||||
# bonsai test temp files
|
||||
src/bonsai/test/files/temp
|
||||
src/bonsai/test/files/basic.ifc.cache.blend
|
||||
src/bonsai/test/files/basic.ifc.cache.sqlite
|
||||
|
||||
# bonsai data
|
||||
src/bonsai/bonsai/bim/data/build/
|
||||
@@ -87,9 +86,7 @@ src/bonsai/bonsai/bim/data/gantt/index.html
|
||||
src/bonsai/bonsai/bim/data/gantt/jsgantt.js
|
||||
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
|
||||
src/bonsai/bonsai/bim/data/webui/static/js/jquery.min.js
|
||||
|
||||
src/bonsai/drawings
|
||||
src/bonsai/layouts
|
||||
src/bonsai/bonsai/bim/data/webui/running_pid.json
|
||||
|
||||
# ifcopenshell swig and compiled files
|
||||
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
|
||||
path = src/ifcopenshell-python/test/Sample-BIM-Files
|
||||
url = https://github.com/IfcOpenShell/ids-test-files
|
||||
[submodule "src/ifcconvert/cityjson"]
|
||||
path = src/ifcconvert/cityjson
|
||||
url = https://github.com/IfcOpenShell/ifc-to-cityjson
|
||||
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
|
||||
path = docs/cpp-api/assets/doxygen-awesome-css
|
||||
url = https://github.com/jothepro/doxygen-awesome-css.git
|
||||
|
||||
@@ -52,7 +52,7 @@ Contents
|
||||
| [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) |
|
||||
| [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/) |
|
||||
|
||||
+34
-710
@@ -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+
|
||||
@@ -85,7 +79,6 @@ option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
|
||||
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
|
||||
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
|
||||
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
|
||||
option(CITYJSON_SUPPORT "Build IfcConvert with CityJSON support (requires CityJSON library)." OFF)
|
||||
option(WITH_RELATIONSHIP_VALIDATION "Build IfcConvert with option to validate geometrical relationships." OFF)
|
||||
option(WITH_ROCKSDB "Support a RocksDB key-value store as a file backend in IfcOpenShell" OFF)
|
||||
option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
|
||||
@@ -110,6 +103,9 @@ endif()
|
||||
|
||||
project(IfcOpenShell VERSION ${RELEASE_VERSION})
|
||||
|
||||
# Make sure CMake modules in this project are found first
|
||||
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
|
||||
|
||||
if(MINIMAL_BUILD)
|
||||
message(STATUS "Setting options for minimal build")
|
||||
set(BUILD_GEOMSERVER OFF)
|
||||
@@ -127,8 +123,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.
|
||||
@@ -200,28 +198,13 @@ if(BUILD_SHARED_LIBS)
|
||||
set(IFCOPENSHELL_LIBRARY_DIR "${LIBDIR}")
|
||||
endif()
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
|
||||
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(EIGEN_DIR)
|
||||
|
||||
if(NOT MINIMAL_BUILD)
|
||||
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
|
||||
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)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
|
||||
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
|
||||
endif()
|
||||
|
||||
# Get a list of all OPTION flags from the CMakeLists.txt
|
||||
@@ -232,53 +215,13 @@ foreach(option_flag IN LISTS option_flags)
|
||||
convert_env_var_to_bool("${option_flag}")
|
||||
endforeach()
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH_BACKUP "${CMAKE_FIND_ROOT_PATH}")
|
||||
|
||||
macro(clear_wasm_sysroot)
|
||||
if(WASM_BUILD)
|
||||
# when using the nix/build-all.py build script we should not
|
||||
# look into the sysroot for most of the dependencies but rather
|
||||
# in the designated build/ folder created by the script.
|
||||
set(CMAKE_FIND_ROOT_PATH "")
|
||||
endif()
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
|
||||
endmacro()
|
||||
|
||||
macro(restore_wasm_sysroot)
|
||||
if(WASM_BUILD)
|
||||
# reset to use sysroot
|
||||
set(CMAKE_FIND_ROOT_PATH "${CMAKE_FIND_ROOT_PATH_BACKUP}")
|
||||
endif()
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endmacro()
|
||||
|
||||
if(WITH_CGAL)
|
||||
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(
|
||||
FATAL_ERROR
|
||||
"CGAL_SUPPORT enabled, but CGAL_INCLUDE_DIR wasn't provided and CGAL package couldn't be found."
|
||||
)
|
||||
endif()
|
||||
message(STATUS "CGAL: found config at '${CGAL_DIR}'.")
|
||||
find_package(CGAL REQUIRED)
|
||||
if(TARGET CGAL::CGAL)
|
||||
link_libraries(CGAL::CGAL)
|
||||
endif()
|
||||
|
||||
add_definitions(-DIFOPSH_WITH_CGAL)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
|
||||
|
||||
if(CITYJSON_SUPPORT)
|
||||
add_definitions(-DIFOPSH_WITH_CITYJSON)
|
||||
endif()
|
||||
|
||||
list(APPEND GEOMETRY_KERNELS cgal)
|
||||
endif()
|
||||
|
||||
@@ -288,7 +231,7 @@ if(WITH_OPENCASCADE)
|
||||
list(APPEND GEOMETRY_KERNELS opencascade)
|
||||
endif()
|
||||
|
||||
if(GLTF_SUPPORT OR CITYJSON_SUPPORT)
|
||||
if(GLTF_SUPPORT)
|
||||
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
|
||||
if(NOT JSON_INCLUDE_DIR)
|
||||
find_package(nlohmann_json CONFIG)
|
||||
@@ -300,9 +243,7 @@ if(GLTF_SUPPORT OR CITYJSON_SUPPORT)
|
||||
endif()
|
||||
|
||||
if(NOT nlohmann_json_DIR)
|
||||
clear_wasm_sysroot()
|
||||
find_path(json_header_path "nlohmann/json.hpp" HINTS ${JSON_INCLUDE_DIR})
|
||||
restore_wasm_sysroot()
|
||||
set(JSON_INCLUDE_DIR ${json_header_path})
|
||||
|
||||
if(json_header_path)
|
||||
@@ -318,49 +259,7 @@ endif()
|
||||
|
||||
# Add USD support to serializers
|
||||
if(USD_SUPPORT)
|
||||
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
|
||||
|
||||
if("${USD_INCLUDE_DIR}" STREQUAL "")
|
||||
find_path(USD_INCLUDE_DIR pxr.h
|
||||
PATHS
|
||||
/usr/include/pxr
|
||||
/usr/local/include/pxr
|
||||
REQUIRED
|
||||
)
|
||||
if(USD_INCLUDE_DIR)
|
||||
message(STATUS "Found USD include files in: ${USD_INCLUDE_DIR}")
|
||||
else()
|
||||
message(FATAL_ERROR "Unable to find USD include directory, specify USD_INCLUDE_DIR manually.")
|
||||
endif()
|
||||
else()
|
||||
set(USD_INCLUDE_DIR ${USD_INCLUDE_DIR} CACHE FILEPATH "USD header files")
|
||||
message(STATUS "Looking for USD include files in: ${USD_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
set(USD_LIBRARIES
|
||||
usd_usd
|
||||
usd_usdGeom
|
||||
usd_usdShade
|
||||
usd_usdLux
|
||||
usd_vt
|
||||
usd_sdf
|
||||
usd_tf
|
||||
usd_gf
|
||||
)
|
||||
|
||||
find_library(USD_LIBRARY
|
||||
NAMES ${USD_LIBRARIES}
|
||||
PATHS ${USD_LIBRARY_DIR})
|
||||
if(USD_LIBRARY)
|
||||
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
|
||||
link_directories(${USD_LIBRARY_DIR})
|
||||
else()
|
||||
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
add_definitions(-DWITH_USD)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
|
||||
find_package(USD REQUIRED)
|
||||
endif(USD_SUPPORT)
|
||||
|
||||
if (WITH_ROCKSDB)
|
||||
@@ -442,41 +341,15 @@ if(USE_MMAP)
|
||||
add_definitions(-DUSE_MMAP)
|
||||
endif()
|
||||
|
||||
clear_wasm_sysroot()
|
||||
find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
|
||||
restore_wasm_sysroot()
|
||||
message(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}")
|
||||
message(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
|
||||
|
||||
if(NOT MINIMAL_BUILD)
|
||||
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
|
||||
clear_wasm_sysroot()
|
||||
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?)
|
||||
find_package(LibXml2 QUIET CONFIG)
|
||||
|
||||
if(NOT LibXml2_FOUND)
|
||||
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
|
||||
find_package(LibXml2 REQUIRED)
|
||||
else()
|
||||
message(STATUS "Found LibXml2 config: ${LibXml2_DIR}")
|
||||
endif()
|
||||
|
||||
if(TARGET LibXml2::LibXml2)
|
||||
# Config mode already gives us the target
|
||||
set(LIBXML2_LIBRARIES LibXml2::LibXml2)
|
||||
get_target_property(LIBXML2_INCLUDE_DIR LibXml2::LibXml2 INTERFACE_INCLUDE_DIRECTORIES)
|
||||
else()
|
||||
# Module mode (Ubuntu)
|
||||
set(LIBXML2_LIBRARIES ${LibXml2_LIBRARIES})
|
||||
set(LIBXML2_INCLUDE_DIR ${LibXml2_INCLUDE_DIRS})
|
||||
endif()
|
||||
else()
|
||||
find_package(LibXml2 REQUIRED)
|
||||
endif()
|
||||
find_package(LibXml2 REQUIRED)
|
||||
endif()
|
||||
restore_wasm_sysroot()
|
||||
endif()
|
||||
|
||||
if(IFCXML_SUPPORT)
|
||||
@@ -485,335 +358,18 @@ if(IFCXML_SUPPORT)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
if(MSVC AND NOT LibXml2_DIR)
|
||||
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
|
||||
endif()
|
||||
|
||||
# Open CASCADE
|
||||
if(WITH_OPENCASCADE)
|
||||
if(OCC_INCLUDE_DIR)
|
||||
set(OCC_INCLUDE_DIR ${OCC_INCLUDE_DIR} CACHE FILEPATH "Open CASCADE header files")
|
||||
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
if(OCC_LIBRARY_DIR)
|
||||
set(OCC_LIBRARY_DIR ${OCC_LIBRARY_DIR} CACHE FILEPATH "Open CASCADE library files")
|
||||
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
# No specific paths specified, try to find package.
|
||||
if(OCC_INCLUDE_DIR AND OCC_LIBRARY_DIR)
|
||||
message(
|
||||
STATUS
|
||||
"Using provided OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') "
|
||||
"and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}')."
|
||||
)
|
||||
# Parse OCC_VERSION_STRING.
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
|
||||
REGEX "#define OCC_VERSION_MAJOR.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
|
||||
REGEX "#define OCC_VERSION_MINOR.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
|
||||
REGEX "#define OCC_VERSION_MAINTENANCE.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
|
||||
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
|
||||
elseif(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
|
||||
# OCE is not supported for find_package, because it's using a different name (`oce`)
|
||||
# and also has an odd directory structure (install/lib/oce-0.18/*.cmake).
|
||||
find_package(OpenCASCADE CONFIG REQUIRED)
|
||||
set(OCC_INCLUDE_DIR ${OpenCASCADE_INCLUDE_DIR})
|
||||
# Do not use OpenCASCADE_LIBRARY_DIR for OCC_LIBRARY_DIR - check target property explicitly.
|
||||
# On Windows there is a case with OpenCASCADE_LIBRARY_DIR points to `lib` folder,
|
||||
# while TKernel is actually in `libi`.
|
||||
get_target_property(TKERNEL_LIB_PATH TKernel LOCATION)
|
||||
get_filename_component(OCC_LIBRARY_DIR "${TKERNEL_LIB_PATH}" DIRECTORY)
|
||||
set(OCC_VERSION_STRING ${OpenCASCADE_VERSION})
|
||||
message(
|
||||
STATUS
|
||||
"Found Open CASCADE package at '${OpenCASCADE_DIR}', "
|
||||
"deducing from it OCC_INCLUDE_DIR: '${OCC_INCLUDE_DIR}' "
|
||||
"and OCC_LIBRARY_DIR: '${OCC_LIBRARY_DIR}'."
|
||||
)
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Couldn't find Open CASCADE installation. "
|
||||
"Either both OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}') "
|
||||
"must be specified or OpenCASCADE package should be discoverable. "
|
||||
"If you're using OCE, then providing a package is not available "
|
||||
"and you need to provide OCE_INCLUDE_DIR and OCE_LIBRARY_DIR directly."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(
|
||||
OPENCASCADE_LIBRARY_NAMES
|
||||
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
|
||||
TKFillet TKXSBase TKOffset TKHLR
|
||||
|
||||
# @todo investigate the exact conditions when this is necessary
|
||||
TKBin
|
||||
)
|
||||
|
||||
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OPENCASCADE_LIBRARY_NAMES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
|
||||
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OPENCASCADE_LIBRARY_NAMES TKDESTEP TKDEIGES)
|
||||
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
|
||||
clear_wasm_sysroot()
|
||||
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
restore_wasm_sysroot()
|
||||
|
||||
if(libTKernel)
|
||||
message(STATUS "Required Open Cascade Library files found")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Unable to find Open Cascade library files in OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}'), aborting"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Use the found libTKernel as a template for all other OCC libraries
|
||||
# TODO Extract this into macro/function
|
||||
foreach(lib ${OPENCASCADE_LIBRARY_NAMES})
|
||||
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
|
||||
string(REPLACE TKerneld "${lib}" lib_path "${libTKernel}")
|
||||
string(REPLACE TKernel "${lib}" lib_path "${lib_path}")
|
||||
list(APPEND OPENCASCADE_LIBRARIES "${lib_path}")
|
||||
endforeach()
|
||||
|
||||
if(MSVC)
|
||||
add_definitions(-DHAVE_NO_DLL)
|
||||
add_debug_variants(OPENCASCADE_LIBRARIES "${OPENCASCADE_LIBRARIES}" d)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# OCC might require linking to Winsock depending on the version and build configuration
|
||||
list(APPEND OPENCASCADE_LIBRARIES ws2_32.lib)
|
||||
endif()
|
||||
|
||||
# Make sure cross-referenced symbols between static OCC libraries get
|
||||
# resolved. Also add thread and rt libraries.
|
||||
get_filename_component(libTKernelExt ${libTKernel} EXT)
|
||||
if("${libTKernelExt}" STREQUAL ".a")
|
||||
set(OCCT_STATIC ON)
|
||||
endif()
|
||||
|
||||
if(OCCT_STATIC)
|
||||
find_package(Threads)
|
||||
|
||||
if(WASM_BUILD)
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
# OPENCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
|
||||
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
|
||||
if(APPLE)
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
set(OPENCASCADE_LIBRARIES -Wl,--start-group ${OPENCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT APPLE AND NOT WIN32)
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} "rt")
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} "dl")
|
||||
endif()
|
||||
endif()
|
||||
find_package(OpenCASCADE REQUIRED)
|
||||
endif(WITH_OPENCASCADE)
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
if(COLLADA_SUPPORT AND (NOT OPENCOLLADA_INCLUDE_DIR AND NOT OPENCOLLADA_LIBRARY_DIR))
|
||||
# If package is found, automatically sets
|
||||
# OPENCOLLADA_INCLUDE_DIRS and OPENCOLLADA_LIBRARIES (list of targets, not paths).
|
||||
find_package(OpenCOLLADA CONFIG)
|
||||
if(OpenCOLLADA_DIR)
|
||||
message(STATUS "Found OpenCOLLADA: '${OpenCOLLADA_DIR}'.")
|
||||
set(OPENCOLLADA_FOUND TRUE)
|
||||
else()
|
||||
message(STATUS "OpenCOLLADA package not found, falling back to manual search.")
|
||||
endif()
|
||||
if(COLLADA_SUPPORT)
|
||||
find_package(OpenCOLLADA REQUIRED)
|
||||
endif()
|
||||
if(COLLADA_SUPPORT AND NOT OpenCOLLADA_DIR)
|
||||
# Find OpenCOLLADA
|
||||
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No OpenCOLLADA include directory specified")
|
||||
set(OPENCOLLADA_INCLUDE_DIR "/usr/include/opencollada" CACHE FILEPATH "OpenCOLLADA header files")
|
||||
else()
|
||||
set(OPENCOLLADA_INCLUDE_DIR "${OPENCOLLADA_INCLUDE_DIR}" CACHE FILEPATH "OpenCOLLADA header files")
|
||||
endif()
|
||||
|
||||
if("${OPENCOLLADA_LIBRARY_DIR}" STREQUAL "")
|
||||
message(STATUS "No OpenCOLLADA library directory specified")
|
||||
find_library(OPENCOLLADA_FRAMEWORK_LIB NAMES OpenCOLLADAFramework
|
||||
PATHS /usr/lib64/opencollada /usr/lib/opencollada /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib)
|
||||
get_filename_component(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_FRAMEWORK_LIB} PATH)
|
||||
endif()
|
||||
|
||||
find_library(OpenCOLLADAFramework NAMES OpenCOLLADAFramework OpenCOLLADAFrameworkd PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
|
||||
if(OpenCOLLADAFramework)
|
||||
message(STATUS "OpenCOLLADA library files found")
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA libraries. "
|
||||
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
|
||||
endif()
|
||||
|
||||
set(OPENCOLLADA_LIBRARY_DIR "${OPENCOLLADA_LIBRARY_DIR}" CACHE FILEPATH "OpenCOLLADA library files")
|
||||
|
||||
set(OPENCOLLADA_INCLUDE_DIRS "${OPENCOLLADA_INCLUDE_DIR}/COLLADABaseUtils" "${OPENCOLLADA_INCLUDE_DIR}/COLLADAStreamWriter")
|
||||
|
||||
find_file(COLLADASWStreamWriter_h "COLLADASWStreamWriter.h" ${OPENCOLLADA_INCLUDE_DIRS})
|
||||
|
||||
if(COLLADASWStreamWriter_h)
|
||||
message(STATUS "OpenCOLLADA header files found")
|
||||
set(OPENCOLLADA_FOUND TRUE)
|
||||
|
||||
set(OPENCOLLADA_LIBRARY_NAMES
|
||||
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
|
||||
OpenCOLLADAStreamWriter UTF buffer ftoa
|
||||
)
|
||||
|
||||
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
|
||||
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
|
||||
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
|
||||
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
|
||||
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
|
||||
endforeach()
|
||||
|
||||
if("${PCRE_LIBRARY_DIR}" STREQUAL "")
|
||||
if(WIN32)
|
||||
find_library(pcre_library NAMES pcre pcred PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
else()
|
||||
find_library(pcre_library NAMES pcre PATHS ${OPENCOLLADA_LIBRARY_DIR})
|
||||
endif()
|
||||
|
||||
get_filename_component(PCRE_LIBRARY_DIR ${pcre_library} PATH)
|
||||
else()
|
||||
find_library(pcre_library NAMES pcre pcred PATHS ${PCRE_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
endif()
|
||||
|
||||
if(pcre_library)
|
||||
set(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_LIBRARY_DIR} ${PCRE_LIBRARY_DIR})
|
||||
|
||||
if(MSVC)
|
||||
# Add release lib regardless whether release or debug found. Debug version will be appended below.
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${PCRE_LIBRARY_DIR}/pcre.lib")
|
||||
else()
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${pcre_library}")
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find PCRE. "
|
||||
"Disable COLLADA_SUPPORT or fix PCRE_LIBRARY_DIR path to proceed.")
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
add_debug_variants(OPENCOLLADA_LIBRARIES "${OPENCOLLADA_LIBRARIES}" d)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA headers. "
|
||||
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
|
||||
endif()
|
||||
endif(COLLADA_SUPPORT AND NOT OpenCOLLADA_DIR)
|
||||
if(COLLADA_SUPPORT AND OPENCOLLADA_FOUND)
|
||||
add_definitions(-DWITH_OPENCOLLADA)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
|
||||
endif()
|
||||
|
||||
|
||||
if(HDF5_SUPPORT)
|
||||
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 include directory specified")
|
||||
else()
|
||||
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
|
||||
endif()
|
||||
|
||||
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 library directory specified")
|
||||
else()
|
||||
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
|
||||
endif()
|
||||
|
||||
if(HDF5_LIBRARY_DIR)
|
||||
# result of the HDF5 ctest package
|
||||
# Find zlib using cmake find_library. How should this be implemented?
|
||||
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
|
||||
if("$ENV{CONDA_BUILD}" STREQUAL "")
|
||||
# result of the HDF5 ctest package
|
||||
if(WIN32)
|
||||
set(zlib_post lib)
|
||||
set(lib_ext lib)
|
||||
else()
|
||||
set(lib_ext a)
|
||||
endif()
|
||||
|
||||
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
set(debug_postfix "_debug")
|
||||
endif()
|
||||
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
|
||||
)
|
||||
|
||||
else()
|
||||
message(STATUS "Packaging hdf5 and zlib for conda distribution")
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
else()
|
||||
# linux and windows
|
||||
# Find HDF5 package
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
# Find ZLIB package
|
||||
find_package(ZLIB REQUIRED)
|
||||
# Include directories
|
||||
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
|
||||
# Link libraries
|
||||
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
|
||||
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
|
||||
# First try to find it as a config.
|
||||
find_package(HDF5 CONFIG)
|
||||
if(HDF5_DIR)
|
||||
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
|
||||
set(HDF5_LIBRARIES hdf5_cpp-static)
|
||||
else()
|
||||
# If it failed, still try to find as a module.
|
||||
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
|
||||
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
|
||||
find_package(HDF5 COMPONENTS CXX)
|
||||
if(NOT HDF5_INCLUDE_DIR)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
|
||||
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
|
||||
"Also could not find HDF5 package (neither module or config)."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
|
||||
add_definitions(-DWITH_HDF5)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
|
||||
@@ -930,25 +486,12 @@ else()
|
||||
endif(MSVC)
|
||||
|
||||
|
||||
# Ensure other dependencies are provided.
|
||||
if(NOT EXISTS "${EIGEN_DIR}")
|
||||
find_package(Eigen3 CONFIG)
|
||||
if(Eigen3_DIR)
|
||||
message(STATUS "Eigen3: found config at '${Eigen3_DIR}'.")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"EIGEN_DIR is not provided or provided folder doesn't exist (current value: '${EIGEN_DIR}'). "
|
||||
"Also couldn't find Eigen3 as a package."
|
||||
)
|
||||
endif()
|
||||
link_libraries(Eigen3::Eigen)
|
||||
endif()
|
||||
find_package(Eigen3 REQUIRED)
|
||||
link_libraries(Eigen3::Eigen)
|
||||
|
||||
|
||||
include_directories(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS}
|
||||
include_directories(${INCLUDE_DIRECTORIES} ${OPENCOLLADA_INCLUDE_DIRS}
|
||||
${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${HDF5_INCLUDE_DIR}
|
||||
${EIGEN_DIR} ${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
|
||||
${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
|
||||
${TBB_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
@@ -1037,6 +580,7 @@ if(NOT Boost_VERSION LESS 105800)
|
||||
add_definitions(-DBOOST_OPTIONAL_USE_OLD_DEFINITION_OF_NONE)
|
||||
endif()
|
||||
|
||||
add_subdirectory(../src/ifcparse ifcparse)
|
||||
set(IFCOPENSHELL_LIBRARIES IfcParse)
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
@@ -1047,84 +591,31 @@ if(BUILD_IFCGEOM)
|
||||
endif()
|
||||
|
||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
set(SERIALIZER_SCHEMA_LIBRARIES ${SERIALIZER_SCHEMA_LIBRARIES} Serializers_ifc${schema})
|
||||
endforeach()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} Serializers ${SERIALIZER_SCHEMA_LIBRARIES})
|
||||
|
||||
if(WITH_OPENCASCADE)
|
||||
if(WITH_OPENCASCADE)
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
set(GEOM_SERIALIZER_SCHEMA_LIBRARIES ${GEOM_SERIALIZER_SCHEMA_LIBRARIES} GeometrySerializers_ifc${schema})
|
||||
|
||||
add_library(geometry_serializer_ifc${schema} STATIC ../src/ifcgeom/Serialization/schema/Serialization.cpp)
|
||||
add_library(geometry_serializer_ifc${schema} OBJECT ../src/ifcgeom/Serialization/schema/Serialization.cpp)
|
||||
set_target_properties(geometry_serializer_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
target_link_libraries(geometry_serializer_ifc${schema} ${OPENCASCADE_LIBRARIES})
|
||||
target_link_libraries(geometry_serializer_ifc${schema} ${OpenCASCADE_LIBRARIES})
|
||||
list(APPEND geometry_serializer_libraries geometry_serializer_ifc${schema})
|
||||
endforeach()
|
||||
|
||||
add_library(geometry_serializer STATIC ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
|
||||
add_library(geometry_serializer OBJECT ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
|
||||
set_target_properties(geometry_serializer PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS")
|
||||
target_link_libraries(geometry_serializer ${geometry_serializer_libraries} IfcParse)
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} geometry_serializer ${geometry_serializer_libraries})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# IfcParse
|
||||
file(GLOB IFCPARSE_H_FILES_ALL ../src/ifcparse/*.h)
|
||||
file(GLOB IFCPARSE_CPP_FILES_ALL ../src/ifcparse/*.cpp)
|
||||
|
||||
foreach(file ${IFCPARSE_H_FILES_ALL})
|
||||
get_filename_component(filename "${file}" NAME)
|
||||
|
||||
if(NOT "${filename}" MATCHES "[0-9]")
|
||||
list(APPEND IFCPARSE_H_FILES "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(file ${IFCPARSE_CPP_FILES_ALL})
|
||||
get_filename_component(filename "${file}" NAME)
|
||||
|
||||
if(NOT "${filename}" MATCHES "[0-9]")
|
||||
list(APPEND IFCPARSE_CPP_FILES "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
list(APPEND IFCPARSE_H_FILES
|
||||
../src/ifcparse/Ifc${schema}.h
|
||||
../src/ifcparse/Ifc${schema}-definitions.h
|
||||
)
|
||||
list(APPEND IFCPARSE_CPP_FILES
|
||||
../src/ifcparse/Ifc${schema}.cpp
|
||||
../src/ifcparse/Ifc${schema}-schema.cpp
|
||||
)
|
||||
endforeach()
|
||||
|
||||
set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
|
||||
|
||||
add_library(IfcParse ${IFCPARSE_FILES})
|
||||
target_link_libraries(IfcParse ${STDCPPFS})
|
||||
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
|
||||
if(LibXml2_DIR)
|
||||
target_compile_definitions(IfcParse PRIVATE ${LIBXML2_DEFINITIONS})
|
||||
endif()
|
||||
if(WASM_BUILD)
|
||||
target_link_libraries(IfcParse ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
# CGAL::CGAL target already has dependencies resolved.
|
||||
if(WITH_CGAL AND CGAL_DIR)
|
||||
set(CGAL_LIBRARIES CGAL::CGAL)
|
||||
message(STATUS "Using found CGAL package at '${CGAL_DIR}'")
|
||||
elseif(WITH_CGAL AND NOT CGAL_DIR)
|
||||
clear_wasm_sysroot()
|
||||
find_library(libGMP NAMES gmp mpir PATHS ${GMP_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
find_library(libMPFR NAMES mpfr PATHS ${MPFR_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
restore_wasm_sysroot()
|
||||
if(NOT libGMP)
|
||||
message(FATAL_ERROR "Unable to find GMP library files, aborting")
|
||||
endif()
|
||||
@@ -1136,33 +627,8 @@ if(BUILD_IFCGEOM)
|
||||
list(APPEND CGAL_LIBRARIES "${libGMP}")
|
||||
endif()
|
||||
|
||||
foreach(kernel ${GEOMETRY_KERNELS})
|
||||
string(TOUPPER ${kernel} KERNEL_UPPER)
|
||||
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/kernels/${kernel}/*.h)
|
||||
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/kernels/${kernel}/*.cpp)
|
||||
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
|
||||
|
||||
add_library(geometry_kernel_${kernel} ${IFCGEOM_FILES} ../src/ifcgeom/kernels/ifc_geomlibrary_api.h)
|
||||
set_property(TARGET geometry_kernel_${kernel} APPEND PROPERTY COMPILE_FLAGS "-DIFC_GEOMLIBRARY_EXPORTS")
|
||||
# needed?
|
||||
# if(NOT WASM_BUILD)
|
||||
# endif()
|
||||
target_link_libraries(geometry_kernel_${kernel} ${${KERNEL_UPPER}_LIBRARIES} IfcGeom IfcParse)
|
||||
list(APPEND kernel_libraries geometry_kernel_${kernel})
|
||||
|
||||
if(${kernel} STREQUAL "cgal")
|
||||
set_property(TARGET geometry_kernel_${kernel} APPEND_STRING PROPERTY COMPILE_FLAGS " -DCGAL_HAS_THREADS")
|
||||
|
||||
add_library(geometry_kernel_${kernel}_simple ${IFCGEOM_FILES})
|
||||
set_target_properties(geometry_kernel_${kernel}_simple PROPERTIES COMPILE_FLAGS "-DIFC_GEOMLIBRARY_EXPORTS -DIFOPSH_SIMPLE_KERNEL -DCGAL_HAS_THREADS")
|
||||
# needed?
|
||||
# if(NOT WASM_BUILD)
|
||||
# endif()
|
||||
target_link_libraries(geometry_kernel_${kernel}_simple ${${KERNEL_UPPER}_LIBRARIES} IfcGeom IfcParse)
|
||||
list(APPEND kernel_libraries geometry_kernel_${kernel}_simple)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_subdirectory(../src/ifcgeom ifcgeom)
|
||||
|
||||
# IfcGeom
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
file(GLOB IFCGEOM_I_FILES ../src/ifcgeom/mapping/*.i)
|
||||
@@ -1170,12 +636,9 @@ if(BUILD_IFCGEOM)
|
||||
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/mapping/*.cpp)
|
||||
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES} ${IFCGEOM_I_FILES})
|
||||
|
||||
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
|
||||
add_library(geometry_mapping_ifc${schema} OBJECT ${IFCGEOM_FILES})
|
||||
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
|
||||
if (NOT BUILD_SHARED_LIBS)
|
||||
target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
|
||||
endif()
|
||||
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
|
||||
endforeach()
|
||||
|
||||
@@ -1191,134 +654,22 @@ if(BUILD_IFCGEOM)
|
||||
find_package(Threads)
|
||||
endif()
|
||||
|
||||
target_link_libraries(IfcGeom IfcParse ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT})
|
||||
target_link_libraries(IfcGeom IfcParse ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT} ${OpenCASCADE_LIBRARIES})
|
||||
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
# Serializers
|
||||
file(GLOB SERIALIZERS_H_FILES ../src/serializers/*.h)
|
||||
file(GLOB SERIALIZERS_CPP_FILES ../src/serializers/*.cpp)
|
||||
set(SERIALIZERS_FILES ${SERIALIZERS_H_FILES} ${SERIALIZERS_CPP_FILES})
|
||||
file(GLOB SERIALIZERS_S_H_FILES ../src/serializers/schema_dependent/*.h)
|
||||
file(GLOB SERIALIZERS_S_CPP_FILES ../src/serializers/schema_dependent/*.cpp)
|
||||
set(SERIALIZERS_S_FILES ${SERIALIZERS_S_H_FILES} ${SERIALIZERS_S_CPP_FILES})
|
||||
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
|
||||
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DSERIALIZERS_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
|
||||
if(WASM_BUILD)
|
||||
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(Serializers_ifc${schema} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(Serializers ${SERIALIZERS_FILES})
|
||||
set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DSERIALIZERS_EXPORTS" VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
|
||||
if(WITH_PROJ)
|
||||
target_compile_definitions(Serializers PRIVATE "WITH_PROJ")
|
||||
if (PROJ_STATIC)
|
||||
target_compile_definitions(Serializers PRIVATE "PROJ_DLL=")
|
||||
endif()
|
||||
target_include_directories(Serializers PRIVATE ${PROJ_INCLUDE_DIR} ${SQLITE_INCLUDE_DIR})
|
||||
target_link_libraries(Serializers ${PROJ_LIBRARIES})
|
||||
endif()
|
||||
|
||||
target_link_libraries(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES} IfcGeom ${OPENCASCADE_LIBRARIES} ${kernel_libraries} IfcParse)
|
||||
|
||||
add_subdirectory(../src/serializers serializers)
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} ${SERIALIZER_SCHEMA_LIBRARIES})
|
||||
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
|
||||
if(BUILD_CONVERT)
|
||||
if(WITH_CGAL AND CITYJSON_SUPPORT)
|
||||
message(STATUS "Building CityJSON support")
|
||||
set(CITYJSON_CONVERT_FILES
|
||||
../src/ifcconvert/cityjson/geobim.cpp
|
||||
../src/ifcconvert/cityjson/global_execution_context.cpp
|
||||
../src/ifcconvert/cityjson/opening_collector.cpp
|
||||
../src/ifcconvert/cityjson/processing.cpp
|
||||
../src/ifcconvert/cityjson/radius_comparison.cpp
|
||||
../src/ifcconvert/cityjson/radius_execution_context.cpp
|
||||
../src/ifcconvert/cityjson/settings.cpp
|
||||
../src/ifcconvert/cityjson/writer.cpp
|
||||
)
|
||||
add_library(cityjson_converter ${CITYJSON_CONVERT_FILES})
|
||||
target_include_directories(cityjson_converter PRIVATE ../src)
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} cityjson_converter)
|
||||
|
||||
install(TARGETS cityjson_converter
|
||||
ARCHIVE DESTINATION ${LIBDIR}
|
||||
LIBRARY DESTINATION ${LIBDIR}
|
||||
)
|
||||
|
||||
add_executable(cityjson_converter_exe ${CITYJSON_CONVERT_FILES})
|
||||
set_target_properties(cityjson_converter_exe PROPERTIES COMPILE_FLAGS "-DCITYJSON_EXECUTABLE")
|
||||
target_include_directories(cityjson_converter_exe PRIVATE ../src)
|
||||
target_link_libraries(cityjson_converter_exe ${IFCOPENSHELL_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES} ${HDF5_LIBRARIES} ${USD_LIBRARIES})
|
||||
|
||||
install(TARGETS cityjson_converter_exe
|
||||
RUNTIME DESTINATION ${BINDIR}
|
||||
)
|
||||
endif()
|
||||
|
||||
# IfcConvert
|
||||
if (WITH_RELATIONSHIP_VALIDATION)
|
||||
file(GLOB IFCCONVERT_CPP_FILES ../src/ifcconvert/*.cpp)
|
||||
file(GLOB IFCCONVERT_H_FILES ../src/ifcconvert/*.h)
|
||||
else()
|
||||
file(GLOB IFCCONVERT_CPP_FILES ../src/ifcconvert/IfcConvert.cpp)
|
||||
file(GLOB IFCCONVERT_H_FILES)
|
||||
endif()
|
||||
set(IFCCONVERT_FILES ${IFCCONVERT_CPP_FILES} ${IFCCONVERT_H_FILES})
|
||||
add_executable(IfcConvert ${IFCCONVERT_FILES})
|
||||
|
||||
target_link_libraries(IfcConvert IfcGeom IfcParse Serializers ${kernel_libraries} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES} ${HDF5_LIBRARIES} ${USD_LIBRARIES})
|
||||
if (WITH_RELATIONSHIP_VALIDATION)
|
||||
set_property(TARGET IfcConvert APPEND_STRING PROPERTY COMPILE_FLAGS " -DWITH_RELATIONSHIP_VALIDATION")
|
||||
endif()
|
||||
|
||||
if(WITH_CGAL AND CITYJSON_SUPPORT)
|
||||
set_property(TARGET IfcConvert APPEND_STRING PROPERTY COMPILE_FLAGS " -DIFOPSH_WITH_CITYJSON")
|
||||
endif()
|
||||
|
||||
if((NOT WIN32) AND BUILD_SHARED_LIBS)
|
||||
# Only set RPATHs when building shared libraries (i.e. IfcParse and
|
||||
# IfcGeom are dynamically linked). Not necessarily a perfect solution
|
||||
# but probably a good indication of whether RPATHs are necessary.
|
||||
SET_INSTALL_RPATHS(IfcConvert "${IFCOPENSHELL_LIBRARY_DIR};${OCC_LIBRARY_DIR};${Boost_LIBRARY_DIRS};${OPENCOLLADA_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
install(TARGETS IfcConvert
|
||||
ARCHIVE DESTINATION ${LIBDIR}
|
||||
LIBRARY DESTINATION ${LIBDIR}
|
||||
RUNTIME DESTINATION ${BINDIR}
|
||||
)
|
||||
add_subdirectory(../src/ifcconvert ifcconvert)
|
||||
endif(BUILD_CONVERT)
|
||||
|
||||
# IfcGeomServer
|
||||
if(BUILD_GEOMSERVER)
|
||||
|
||||
if(NOT WITH_OPENCASCADE)
|
||||
message(FATAL_ERROR "Open CASCADE is required to build IfcGeomServer.")
|
||||
endif()
|
||||
|
||||
file(GLOB CPP_FILES ../src/ifcgeomserver/*.cpp)
|
||||
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
|
||||
set(SOURCE_FILES ${CPP_FILES} ${H_FILES})
|
||||
add_executable(IfcGeomServer ${SOURCE_FILES})
|
||||
target_link_libraries(IfcGeomServer IfcGeom IfcParse Serializers ${kernel_libraries} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES})
|
||||
|
||||
if((NOT WIN32) AND BUILD_SHARED_LIBS)
|
||||
SET_INSTALL_RPATHS(IfcGeomServer "${IFCOPENSHELL_LIBRARY_DIR};${OCC_LIBRARY_DIR};${Boost_LIBRARY_DIRS}")
|
||||
endif()
|
||||
|
||||
install(TARGETS IfcGeomServer
|
||||
ARCHIVE DESTINATION ${LIBDIR}
|
||||
LIBRARY DESTINATION ${LIBDIR}
|
||||
RUNTIME DESTINATION ${BINDIR}
|
||||
)
|
||||
add_subdirectory(../src/ifcgeomserver ifcgeomserver)
|
||||
endif(BUILD_GEOMSERVER)
|
||||
|
||||
if(ADD_COMMIT_SHA)
|
||||
@@ -1405,17 +756,6 @@ if(BUILD_QTVIEWER)
|
||||
add_subdirectory(../src/qtviewer qtviewer)
|
||||
endif()
|
||||
|
||||
# CMake installation targets
|
||||
install(FILES ${IFCPARSE_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/ifcparse
|
||||
)
|
||||
|
||||
install(TARGETS IfcParse
|
||||
ARCHIVE DESTINATION ${LIBDIR}
|
||||
LIBRARY DESTINATION ${LIBDIR}
|
||||
RUNTIME DESTINATION ${BINDIR}
|
||||
)
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
# install(FILES ${IFCGEOM_H_FILES}
|
||||
# DESTINATION ${INCLUDEDIR}/ifcgeom
|
||||
@@ -1444,22 +784,6 @@ if(BUILD_IFCGEOM)
|
||||
)
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
if(BUILD_CONVERT)
|
||||
install(TARGETS Serializers ${SERIALIZER_SCHEMA_LIBRARIES}
|
||||
ARCHIVE DESTINATION ${LIBDIR}
|
||||
LIBRARY DESTINATION ${LIBDIR}
|
||||
RUNTIME DESTINATION ${BINDIR}
|
||||
)
|
||||
|
||||
install(FILES ${SERIALIZERS_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/serializers/
|
||||
)
|
||||
|
||||
install(FILES ${SERIALIZERS_S_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/serializers/schema_dependent
|
||||
)
|
||||
endif(BUILD_CONVERT)
|
||||
|
||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
if(WITH_OPENCASCADE)
|
||||
install(TARGETS geometry_serializer ${geometry_serializer_libraries}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"IFCXML_SUPPORT": "ON",
|
||||
"HDF5_SUPPORT": "ON",
|
||||
"SCHEMA_VERSIONS": "4x3_add2",
|
||||
"CITYJSON_SUPPORT": "OFF",
|
||||
"CMAKE_GENERATOR_PLATFORM": "",
|
||||
"CMAKE_GENERATOR_TOOLSET": ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `CGAL_INCLUDE_DIR`
|
||||
# - `CGAL_LIBRARY_DIR`
|
||||
# - `GMP_INCLUDE_DIR`
|
||||
# - `GMP_LIBRARY_DIR`
|
||||
# - `MPFR_INCLUDE_DIR`
|
||||
# - `MPFR_LIBRARY_DIR`
|
||||
# If input variables are not specified, try to find HDF5 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `CGAL_INCLUDE_DIR`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
|
||||
|
||||
if(NOT CGAL_INCLUDE_DIR)
|
||||
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
|
||||
# and sometiems it's getting in the way.
|
||||
if(NOT DEFINED Boost_USE_STATIC_LIBS)
|
||||
set(CGAL_Boost_USE_STATIC_LIBS OFF)
|
||||
else()
|
||||
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
|
||||
endif()
|
||||
find_package(CGAL CONFIG REQUIRED)
|
||||
if(NOT CGAL_DIR)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"CGAL_SUPPORT enabled, but CGAL_INCLUDE_DIR wasn't provided and CGAL package couldn't be found."
|
||||
)
|
||||
endif()
|
||||
message(STATUS "CGAL: found config at '${CGAL_DIR}'.")
|
||||
endif()
|
||||
|
||||
add_definitions(-DIFOPSH_WITH_CGAL)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
|
||||
@@ -0,0 +1,27 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `EIGEN_DIR`
|
||||
# If input variables are not specified, try to find Eigen3 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output targets:
|
||||
# - `Eigen3::Eigen`
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(EIGEN_DIR)
|
||||
|
||||
if(EXISTS "${EIGEN_DIR}")
|
||||
# Mimic Eigen3Config.cmake target.
|
||||
add_library(Eigen3::Eigen INTERFACE IMPORTED)
|
||||
target_include_directories(Eigen3::Eigen INTERFACE "${EIGEN_DIR}")
|
||||
else()
|
||||
find_package(Eigen3 CONFIG)
|
||||
if(Eigen3_DIR)
|
||||
message(STATUS "Eigen3: found config at '${Eigen3_DIR}'.")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"EIGEN_DIR is not provided or provided folder doesn't exist (current value: '${EIGEN_DIR}'). "
|
||||
"Also couldn't find Eigen3 as a package."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,109 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `HDF5_INCLUDE_DIR`
|
||||
# - `HDF5_LIBRARY_DIR`
|
||||
# - `HDF5_LIBRARIES`
|
||||
# If input variables are not specified, try to find HDF5 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `HDF5_INCLUDE_DIR`
|
||||
# - `HDF5_LIBRARY_DIR`
|
||||
# - `HDF5_LIBRARIES`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
|
||||
|
||||
# To avoid cyclic calls to this file
|
||||
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
|
||||
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 include directory specified")
|
||||
else()
|
||||
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
|
||||
endif()
|
||||
|
||||
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 library directory specified")
|
||||
else()
|
||||
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
|
||||
endif()
|
||||
|
||||
if(HDF5_LIBRARY_DIR)
|
||||
# result of the HDF5 ctest package
|
||||
# Find zlib using cmake find_library. How should this be implemented?
|
||||
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
|
||||
if("$ENV{CONDA_BUILD}" STREQUAL "")
|
||||
# result of the HDF5 ctest package
|
||||
if(WIN32)
|
||||
set(zlib_post lib)
|
||||
set(lib_ext lib)
|
||||
else()
|
||||
set(lib_ext a)
|
||||
endif()
|
||||
|
||||
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
set(debug_postfix "_debug")
|
||||
endif()
|
||||
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
|
||||
)
|
||||
|
||||
else()
|
||||
message(STATUS "Packaging hdf5 and zlib for conda distribution")
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
else()
|
||||
# linux and windows
|
||||
# Find HDF5 package
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
# Find ZLIB package
|
||||
find_package(ZLIB REQUIRED)
|
||||
# Include directories
|
||||
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
|
||||
# Link libraries
|
||||
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
|
||||
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
|
||||
# First try to find it as a config.
|
||||
find_package(HDF5 CONFIG)
|
||||
if(HDF5_DIR)
|
||||
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
|
||||
set(HDF5_LIBRARIES hdf5_cpp-static)
|
||||
else()
|
||||
# If it failed, still try to find as a module.
|
||||
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
|
||||
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
|
||||
find_package(HDF5 COMPONENTS CXX)
|
||||
if(NOT HDF5_INCLUDE_DIR)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
|
||||
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
|
||||
"Also could not find HDF5 package (neither module or config)."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Restore module path.
|
||||
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `LIBXML2_INCLUDE_DIR`
|
||||
# - `LIBXML2_LIBRARIES`
|
||||
# If input variables are not specified, try to find LibXml2 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `LIBXML2_INCLUDE_DIR`
|
||||
# - `LIBXML2_LIBRARIES`
|
||||
#
|
||||
|
||||
# To avoid cyclic calls to this file
|
||||
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
|
||||
|
||||
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
|
||||
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
|
||||
# CONFIG is provided using root path, so no need to clear sysroot here.
|
||||
find_package(LibXml2 QUIET CONFIG)
|
||||
|
||||
if(NOT LibXml2_FOUND)
|
||||
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
|
||||
find_package(LibXml2 REQUIRED)
|
||||
else()
|
||||
message(STATUS "Found LibXml2 config: ${LibXml2_DIR}")
|
||||
endif()
|
||||
|
||||
if(TARGET LibXml2::LibXml2)
|
||||
# Config mode already gives us the target
|
||||
set(LIBXML2_LIBRARIES LibXml2::LibXml2)
|
||||
get_target_property(LIBXML2_INCLUDE_DIR LibXml2::LibXml2 INTERFACE_INCLUDE_DIRECTORIES)
|
||||
else()
|
||||
# Module mode (Ubuntu)
|
||||
set(LIBXML2_LIBRARIES ${LibXml2_LIBRARIES})
|
||||
set(LIBXML2_INCLUDE_DIR ${LibXml2_INCLUDE_DIRS})
|
||||
endif()
|
||||
else()
|
||||
find_package(LibXml2 REQUIRED)
|
||||
endif()
|
||||
|
||||
if(MSVC AND NOT LibXml2_DIR)
|
||||
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
|
||||
endif()
|
||||
|
||||
# Restore module path.
|
||||
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
@@ -0,0 +1,144 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `OCC_INCLUDE_DIR`
|
||||
# - `OCC_LIBRARY_DIR`
|
||||
# If input variables are not specified, try to find OpenCASCADE config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables
|
||||
# - `OpenCASCADE_LIBRARIES`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
|
||||
|
||||
if(OCC_INCLUDE_DIR)
|
||||
set(OCC_INCLUDE_DIR ${OCC_INCLUDE_DIR} CACHE FILEPATH "Open CASCADE header files")
|
||||
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
if(OCC_LIBRARY_DIR)
|
||||
set(OCC_LIBRARY_DIR ${OCC_LIBRARY_DIR} CACHE FILEPATH "Open CASCADE library files")
|
||||
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
|
||||
if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
|
||||
# OCE is not supported for find_package, because it's using a different name (`oce`)
|
||||
# and also has an odd directory structure (install/lib/oce-0.18/*.cmake).
|
||||
# find_package creates variables:
|
||||
# - `OpenCASCADE_INCLUDE_DIR`
|
||||
# - `OpenCASCADE_LIBRARIES`
|
||||
|
||||
# OpenCASCADE may be built with VTK support. Try to find VTK first to avoid
|
||||
# CMake errors when OpenCASCADE's config references VTK targets.
|
||||
find_package(VTK QUIET)
|
||||
|
||||
find_package(OpenCASCADE CONFIG REQUIRED)
|
||||
message(STATUS "Found OpenCASCADE config: ${OpenCASCADE_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# No specific paths specified, try to find package.
|
||||
if(OCC_INCLUDE_DIR AND OCC_LIBRARY_DIR)
|
||||
message(
|
||||
STATUS
|
||||
"Using provided OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') "
|
||||
"and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}')."
|
||||
)
|
||||
# Parse OCC_VERSION_STRING.
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
|
||||
REGEX "#define OCC_VERSION_MAJOR.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
|
||||
REGEX "#define OCC_VERSION_MINOR.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
|
||||
REGEX "#define OCC_VERSION_MAINTENANCE.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
|
||||
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Couldn't find Open CASCADE installation. "
|
||||
"Either both OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}') "
|
||||
"must be specified or OpenCASCADE package should be discoverable. "
|
||||
"If you're using OCE, then providing a package is not available "
|
||||
"and you need to provide OCE_INCLUDE_DIR and OCE_LIBRARY_DIR directly."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(
|
||||
OpenCASCADE_LIBRARIES
|
||||
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
|
||||
TKFillet TKXSBase TKOffset TKHLR
|
||||
|
||||
# @todo investigate the exact conditions when this is necessary
|
||||
TKBin
|
||||
)
|
||||
|
||||
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OpenCASCADE_LIBRARIES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
|
||||
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OpenCASCADE_LIBRARIES TKDESTEP TKDEIGES)
|
||||
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
|
||||
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
|
||||
if(libTKernel)
|
||||
message(STATUS "Required Open Cascade Library files found")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Unable to find Open Cascade library files in OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}'), aborting"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
add_definitions(-DHAVE_NO_DLL)
|
||||
add_debug_variants(OpenCASCADE_LIBRARIES "${OpenCASCADE_LIBRARIES}" d)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# OCC might require linking to Winsock depending on the version and build configuration
|
||||
list(APPEND OpenCASCADE_LIBRARIES ws2_32.lib)
|
||||
endif()
|
||||
|
||||
# Make sure cross-referenced symbols between static OCC libraries get
|
||||
# resolved. Also add thread and rt libraries.
|
||||
get_filename_component(libTKernelExt ${libTKernel} EXT)
|
||||
if("${libTKernelExt}" STREQUAL ".a")
|
||||
set(OCCT_STATIC ON)
|
||||
endif()
|
||||
|
||||
if(OCCT_STATIC)
|
||||
find_package(Threads)
|
||||
|
||||
if(WASM_BUILD)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
# OpenCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
|
||||
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
|
||||
if(APPLE)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
set(OpenCASCADE_LIBRARIES -Wl,--start-group ${OpenCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT APPLE AND NOT WIN32)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "rt")
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "dl")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(OpenCASCADE_INTERFACE INTERFACE IMPORTED)
|
||||
target_include_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_INCLUDE_DIR}")
|
||||
target_link_libraries(OpenCASCADE_INTERFACE INTERFACE ${OpenCASCADE_LIBRARIES})
|
||||
target_link_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_LIBRARY_DIR}")
|
||||
set(OpenCASCADE_LIBRARIES OpenCASCADE_INTERFACE)
|
||||
@@ -0,0 +1,118 @@
|
||||
|
||||
#
|
||||
# Input variables:
|
||||
# - `OPENCOLLADA_INCLUDE_DIR`
|
||||
# - `OPENCOLLADA_LIBRARY_DIR`
|
||||
# - `PCRE_LIBRARY_DIR`
|
||||
# If input variables are not specified, try to find OpenCOLLADA config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `OPENCOLLADA_INCLUDE_DIR`
|
||||
# - `OPENCOLLADA_LIBRARY_DIR`
|
||||
# - `OPENCOLLADA_LIBRARIES`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
|
||||
|
||||
if(NOT OPENCOLLADA_INCLUDE_DIR AND NOT OPENCOLLADA_LIBRARY_DIR)
|
||||
# If package is found, automatically sets
|
||||
# OPENCOLLADA_INCLUDE_DIRS and OPENCOLLADA_LIBRARIES (list of targets, not paths).
|
||||
find_package(OpenCOLLADA CONFIG)
|
||||
if(OpenCOLLADA_DIR)
|
||||
message(STATUS "Found OpenCOLLADA: '${OpenCOLLADA_DIR}'.")
|
||||
set(OPENCOLLADA_FOUND TRUE)
|
||||
else()
|
||||
message(STATUS "OpenCOLLADA package not found, falling back to manual search.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT OpenCOLLADA_DIR)
|
||||
# Find OpenCOLLADA
|
||||
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No OpenCOLLADA include directory specified")
|
||||
set(OPENCOLLADA_INCLUDE_DIR "/usr/include/opencollada" CACHE FILEPATH "OpenCOLLADA header files")
|
||||
else()
|
||||
set(OPENCOLLADA_INCLUDE_DIR "${OPENCOLLADA_INCLUDE_DIR}" CACHE FILEPATH "OpenCOLLADA header files")
|
||||
endif()
|
||||
|
||||
if("${OPENCOLLADA_LIBRARY_DIR}" STREQUAL "")
|
||||
message(STATUS "No OpenCOLLADA library directory specified")
|
||||
find_library(OPENCOLLADA_FRAMEWORK_LIB NAMES OpenCOLLADAFramework
|
||||
PATHS /usr/lib64/opencollada /usr/lib/opencollada /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib)
|
||||
get_filename_component(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_FRAMEWORK_LIB} PATH)
|
||||
endif()
|
||||
|
||||
find_library(OpenCOLLADAFramework NAMES OpenCOLLADAFramework OpenCOLLADAFrameworkd PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
|
||||
if(OpenCOLLADAFramework)
|
||||
message(STATUS "OpenCOLLADA library files found")
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA libraries. "
|
||||
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
|
||||
endif()
|
||||
|
||||
set(OPENCOLLADA_LIBRARY_DIR "${OPENCOLLADA_LIBRARY_DIR}" CACHE FILEPATH "OpenCOLLADA library files")
|
||||
|
||||
set(OPENCOLLADA_INCLUDE_DIRS "${OPENCOLLADA_INCLUDE_DIR}/COLLADABaseUtils" "${OPENCOLLADA_INCLUDE_DIR}/COLLADAStreamWriter")
|
||||
|
||||
find_file(COLLADASWStreamWriter_h "COLLADASWStreamWriter.h" ${OPENCOLLADA_INCLUDE_DIRS})
|
||||
|
||||
if(COLLADASWStreamWriter_h)
|
||||
message(STATUS "OpenCOLLADA header files found")
|
||||
set(OPENCOLLADA_FOUND TRUE)
|
||||
|
||||
set(OPENCOLLADA_LIBRARY_NAMES
|
||||
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
|
||||
OpenCOLLADAStreamWriter UTF buffer ftoa
|
||||
)
|
||||
|
||||
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
|
||||
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
|
||||
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
|
||||
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
|
||||
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
|
||||
endforeach()
|
||||
|
||||
if("${PCRE_LIBRARY_DIR}" STREQUAL "")
|
||||
if(WIN32)
|
||||
find_library(pcre_library NAMES pcre pcred PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
else()
|
||||
find_library(pcre_library NAMES pcre PATHS ${OPENCOLLADA_LIBRARY_DIR})
|
||||
endif()
|
||||
|
||||
get_filename_component(PCRE_LIBRARY_DIR ${pcre_library} PATH)
|
||||
else()
|
||||
find_library(pcre_library NAMES pcre pcred PATHS ${PCRE_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
endif()
|
||||
|
||||
if(pcre_library)
|
||||
set(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_LIBRARY_DIR} ${PCRE_LIBRARY_DIR})
|
||||
|
||||
if(MSVC)
|
||||
# Add release lib regardless whether release or debug found. Debug version will be appended below.
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${PCRE_LIBRARY_DIR}/pcre.lib")
|
||||
else()
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${pcre_library}")
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find PCRE. "
|
||||
"Disable COLLADA_SUPPORT or fix PCRE_LIBRARY_DIR path to proceed.")
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
add_debug_variants(OPENCOLLADA_LIBRARIES "${OPENCOLLADA_LIBRARIES}" d)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA headers. "
|
||||
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
|
||||
endif()
|
||||
endif(NOT OpenCOLLADA_DIR)
|
||||
|
||||
if(OPENCOLLADA_FOUND)
|
||||
add_definitions(-DWITH_OPENCOLLADA)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
|
||||
endif()
|
||||
@@ -0,0 +1,54 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `USD_INCLUDE_DIR`
|
||||
# - `USD_LIBRARY_DIR`
|
||||
# Input variables could also be provided as environment variables.
|
||||
# TODO: Try to find USD config if varibales are not provided.
|
||||
# TODO: does usd have a config file?
|
||||
#
|
||||
# Output variables:
|
||||
# - `USD_LIBRARIES`
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
|
||||
|
||||
if("${USD_INCLUDE_DIR}" STREQUAL "")
|
||||
find_path(USD_INCLUDE_DIR pxr.h
|
||||
PATHS
|
||||
/usr/include/pxr
|
||||
/usr/local/include/pxr
|
||||
REQUIRED
|
||||
)
|
||||
if(USD_INCLUDE_DIR)
|
||||
message(STATUS "Found USD include files in: ${USD_INCLUDE_DIR}")
|
||||
else()
|
||||
message(FATAL_ERROR "Unable to find USD include directory, specify USD_INCLUDE_DIR manually.")
|
||||
endif()
|
||||
else()
|
||||
set(USD_INCLUDE_DIR ${USD_INCLUDE_DIR} CACHE FILEPATH "USD header files")
|
||||
message(STATUS "Looking for USD include files in: ${USD_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
set(USD_LIBRARIES
|
||||
usd_usd
|
||||
usd_usdGeom
|
||||
usd_usdShade
|
||||
usd_usdLux
|
||||
usd_vt
|
||||
usd_sdf
|
||||
usd_tf
|
||||
usd_gf
|
||||
)
|
||||
|
||||
find_library(USD_LIBRARY
|
||||
NAMES ${USD_LIBRARIES}
|
||||
PATHS ${USD_LIBRARY_DIR})
|
||||
if(USD_LIBRARY)
|
||||
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
|
||||
link_directories(${USD_LIBRARY_DIR})
|
||||
else()
|
||||
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
add_definitions(-DWITH_USD)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
|
||||
@@ -42,7 +42,6 @@ cmake -G "Ninja" ^
|
||||
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
|
||||
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
|
||||
-D CITYJSON_SUPPORT:BOOL=OFF ^
|
||||
../cmake
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
|
||||
@@ -42,7 +42,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
|
||||
-DBUILD_IFCGEOM:BOOL=ON \
|
||||
-DBUILD_GEOMSERVER:BOOL=OFF \
|
||||
-DBOOST_USE_STATIC_LIBS:BOOL=OFF \
|
||||
-DCITYJSON_SUPPORT:BOOL=OFF \
|
||||
./cmake
|
||||
|
||||
ninja
|
||||
|
||||
+199
-120
@@ -18,8 +18,6 @@
|
||||
# #
|
||||
###############################################################################
|
||||
|
||||
# ruff: noqa: UP035
|
||||
|
||||
"""
|
||||
Example usage:
|
||||
# Build all targets by default.
|
||||
@@ -33,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
|
||||
@@ -52,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
|
||||
|
||||
@@ -135,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"
|
||||
@@ -195,19 +201,61 @@ 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.
|
||||
# NOTE: changes in pyodide-build currently got postponed:
|
||||
# https://github.com/pyodide/pyodide-build/pull/249
|
||||
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (99, 0, 0)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -223,9 +271,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:
|
||||
@@ -234,7 +284,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
|
||||
@@ -266,6 +316,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)
|
||||
@@ -282,6 +333,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"),
|
||||
@@ -328,11 +384,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
|
||||
@@ -357,6 +415,23 @@ 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)))))
|
||||
|
||||
@@ -365,16 +440,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)
|
||||
@@ -398,6 +478,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,
|
||||
@@ -497,16 +584,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,
|
||||
@@ -637,7 +735,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)
|
||||
@@ -724,12 +822,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}"
|
||||
@@ -738,8 +837,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}"
|
||||
@@ -819,9 +916,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
|
||||
@@ -834,14 +931,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(
|
||||
@@ -852,11 +943,9 @@ if "pcre2" in targets:
|
||||
download_name=f"pcre2-{PCRE2_VERSION}.tar.bz2",
|
||||
)
|
||||
|
||||
# An issue exists with swig-1.3 and python >= 3.2
|
||||
# Therefore, build a recent copy from source
|
||||
if "swig" in targets:
|
||||
build_dependency(
|
||||
name="swig",
|
||||
name=f"swig-{SWIG_VERSION}",
|
||||
mode="autoconf",
|
||||
build_tool_args=["--disable-ccache", f"--with-pcre2-prefix={DEPS_DIR}/install/pcre2-{PCRE2_VERSION}"],
|
||||
download_url="https://github.com/swig/swig.git",
|
||||
@@ -873,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:
|
||||
@@ -1012,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()
|
||||
@@ -1020,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:
|
||||
@@ -1041,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
|
||||
@@ -1086,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}",
|
||||
@@ -1102,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",
|
||||
@@ -1115,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}",
|
||||
@@ -1249,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:
|
||||
@@ -1267,26 +1378,10 @@ 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",
|
||||
]
|
||||
)
|
||||
cmake_args.append(f"-DCGAL_WITH_GMPXX=Off")
|
||||
|
||||
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.
|
||||
@@ -1307,13 +1402,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}")
|
||||
@@ -1343,7 +1431,10 @@ if "rocksdb" in targets:
|
||||
]
|
||||
)
|
||||
|
||||
if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets):
|
||||
if "swig" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/swig-{SWIG_VERSION}")
|
||||
|
||||
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
|
||||
logger.info("\rConfiguring executables...")
|
||||
|
||||
exec_args = [
|
||||
@@ -1358,17 +1449,14 @@ if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(expl
|
||||
|
||||
logger.info("\rBuilding executables... ")
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], cwd=executables_dir)
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=executables_dir)
|
||||
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
|
||||
|
||||
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)
|
||||
@@ -1382,40 +1470,44 @@ 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] = []
|
||||
if "swig" in targets:
|
||||
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()
|
||||
+ [
|
||||
"-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"}],
|
||||
@@ -1426,7 +1518,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:
|
||||
@@ -1453,32 +1545,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,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)
|
||||
"""
|
||||
)
|
||||
+12
-4
@@ -9,10 +9,8 @@ extend-exclude = '''
|
||||
|src/ifcopenshell-python/ifcopenshell/mvd/*
|
||||
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|
||||
|src/ifc2ca/templates/*
|
||||
|src/ifcconvert/cityjson/*
|
||||
|src/svgfill
|
||||
|src/exterior-shell-extractor
|
||||
|src/pyodide
|
||||
'''
|
||||
|
||||
[tool.pyright]
|
||||
@@ -21,12 +19,11 @@ disableBytesTypePromotions = true
|
||||
reportUnnecessaryTypeIgnoreComment = true
|
||||
|
||||
# Define here general ruff settings,
|
||||
# then they will be inherited projects .toml files.
|
||||
# then they will be inherited by projects' .toml files.
|
||||
# This allows using assuming different Python version for different projects.
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
# Submodules.
|
||||
"src/ifcconvert/cityjson",
|
||||
"src/ifcopenshell-python/ifcopenshell/express",
|
||||
"src/ifcopenshell-python/ifcopenshell/mvd",
|
||||
"src/ifcopenshell-python/ifcopenshell/simple_spf",
|
||||
@@ -62,3 +59,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"]
|
||||
|
||||
+4
-1
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
|
||||
endif
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=6924012
|
||||
OLD:=e8eb5e4
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -155,6 +155,9 @@ endif
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download six --dest=./wheels
|
||||
# Required by drawing module
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
# Required by drawing module for markdown hyperlinks in annotations
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download markdown-it-py --dest=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download mdurl --dest=./wheels
|
||||
# Required by qto and drawing module
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
# Required by the BIM type manager thumbnail generator
|
||||
|
||||
@@ -98,7 +98,7 @@ def initialize_bbim_semver():
|
||||
def get_debug_info():
|
||||
bbim_version = bbim_semver["version"]
|
||||
|
||||
return {
|
||||
debug_info = {
|
||||
"os": platform.system(),
|
||||
"os_version": platform.version(),
|
||||
"python_version": platform.python_version(),
|
||||
@@ -113,6 +113,25 @@ def get_debug_info():
|
||||
"last_error": last_error,
|
||||
}
|
||||
|
||||
# Add .blend file save information
|
||||
if bpy.data.is_saved:
|
||||
debug_info["blend_file_path"] = bpy.data.filepath
|
||||
debug_info["blend_file_dirty"] = bpy.data.is_dirty
|
||||
else:
|
||||
debug_info["blend_file_path"] = "Not saved"
|
||||
debug_info["blend_file_dirty"] = "N/A"
|
||||
|
||||
# Add IFC file information
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
if bim_props.ifc_file:
|
||||
debug_info["ifc_file_path"] = bim_props.ifc_file
|
||||
debug_info["ifc_is_dirty"] = bim_props.is_dirty
|
||||
else:
|
||||
debug_info["ifc_file_path"] = "No IFC loaded"
|
||||
debug_info["ifc_is_dirty"] = "N/A"
|
||||
|
||||
return debug_info
|
||||
|
||||
|
||||
def format_debug_info(info: dict):
|
||||
last_actions = ""
|
||||
|
||||
@@ -21,7 +21,7 @@ import bpy
|
||||
import bpy.utils.previews
|
||||
import importlib
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from . import handler, ui, prop, operator
|
||||
from . import handler, ui, prop, operator, gizmo
|
||||
from typing import Union
|
||||
from collections.abc import Callable
|
||||
|
||||
@@ -148,7 +148,11 @@ classes = [
|
||||
ui.BIM_UL_clipping_plane,
|
||||
ui.BIM_UL_generic,
|
||||
ui.DocPreferences,
|
||||
ui.BIM_ADDON_preferences,
|
||||
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
|
||||
ui.GizmoPreferencesWindow, # Register before GizmoPreferences
|
||||
ui.GizmoPreferencesStair, # Register before GizmoPreferences
|
||||
ui.GizmoPreferences,
|
||||
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
|
||||
# Tabs panel
|
||||
ui.BIM_PT_tabs,
|
||||
# Project overview
|
||||
@@ -202,6 +206,19 @@ classes = [
|
||||
ui.BIM_PT_section_with_cappings,
|
||||
ui.BIM_PT_decorators_overlay,
|
||||
ui.BIM_PT_snappping,
|
||||
# Gizmos
|
||||
gizmo.BIM_OT_gizmo_value_input,
|
||||
gizmo.GizmoArrow,
|
||||
gizmo.GizmoArrow2D,
|
||||
gizmo.GizmoCone,
|
||||
gizmo.GizmoLock,
|
||||
gizmo.GizmoArc,
|
||||
gizmo.GizmoPen,
|
||||
gizmo.GizmoValidate,
|
||||
gizmo.GizmoCancel,
|
||||
gizmo.GizmoPlus,
|
||||
gizmo.GizmoMinus,
|
||||
gizmo.GizmoCycle,
|
||||
]
|
||||
|
||||
for mod in modules.values():
|
||||
@@ -225,14 +242,33 @@ def on_register(scene):
|
||||
is_registering = False
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
# Classes that need to be registered after modules (due to cross-module dependencies)
|
||||
late_classes = (
|
||||
ui.DefaultParameters, # Register before BIM_ADDON_preferences
|
||||
ui.BIM_ADDON_preferences,
|
||||
)
|
||||
|
||||
|
||||
def register_classes(classes_to_register):
|
||||
for cls in classes_to_register:
|
||||
# Prevent crashes in Blender 4.4.0, see #6420.
|
||||
if issubclass(cls, (ImportHelper, ExportHelper)):
|
||||
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
|
||||
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister_classes(classes_to_unregister):
|
||||
for cls in reversed(classes_to_unregister):
|
||||
if getattr(cls, "is_registered", None) is None:
|
||||
bpy.utils.unregister_class(cls)
|
||||
elif cls.is_registered:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
||||
def register():
|
||||
register_classes(classes)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(on_register)
|
||||
bpy.app.handlers.undo_post.append(handler.undo_post)
|
||||
bpy.app.handlers.redo_post.append(handler.redo_post)
|
||||
@@ -256,6 +292,9 @@ def register():
|
||||
for mod in modules.values():
|
||||
mod.register()
|
||||
|
||||
# Delay registering classes that depend on module classes
|
||||
register_classes(late_classes)
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
|
||||
@@ -287,11 +326,7 @@ def unregister():
|
||||
|
||||
bpy.utils.previews.remove(icons)
|
||||
|
||||
for cls in reversed(classes):
|
||||
if getattr(cls, "is_registered", None) is None:
|
||||
bpy.utils.unregister_class(cls)
|
||||
elif cls.is_registered:
|
||||
bpy.utils.unregister_class(cls)
|
||||
unregister_classes(classes)
|
||||
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
|
||||
@@ -306,6 +341,9 @@ def unregister():
|
||||
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
|
||||
bpy.types.STATUSBAR_HT_header.remove(ui.draw_statusbar)
|
||||
|
||||
# Unregister late classes before modules
|
||||
unregister_classes(late_classes)
|
||||
|
||||
for mod in reversed(list(modules.values())):
|
||||
mod.unregister()
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
@@ -54,11 +48,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -253,12 +242,6 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
@@ -300,11 +283,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -377,7 +355,7 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.display.shading.studio_light": "Default",
|
||||
"scene.display.shading.studio_light": "forest.exr",
|
||||
"scene.display.shading.studiolight_background_alpha": 0.0,
|
||||
"scene.display.shading.studiolight_background_blur": 0.0,
|
||||
"scene.display.shading.studiolight_intensity": 0.0,
|
||||
@@ -503,18 +481,12 @@
|
||||
"scene.eevee.shadow_ray_count": 1,
|
||||
"scene.eevee.shadow_step_count": 6,
|
||||
"scene.eevee.shadow_resolution_scale": 1.0,
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
"scene.render.border_min_y": 0.0,
|
||||
"scene.render.dither_intensity": 1.0,
|
||||
"scene.render.engine": "BLENDER_EEVEE_NEXT",
|
||||
"scene.render.engine": "BLENDER_EEVEE",
|
||||
"scene.render.film_transparent": false,
|
||||
"scene.render.filter_size": 1.5,
|
||||
"scene.render.fps": 24,
|
||||
@@ -550,11 +522,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -627,7 +594,7 @@
|
||||
0.800000011920929,
|
||||
0.800000011920929
|
||||
],
|
||||
"scene.display.shading.studio_light": "Default",
|
||||
"scene.display.shading.studio_light": "forest.exr",
|
||||
"scene.display.shading.studiolight_background_alpha": 0.0,
|
||||
"scene.display.shading.studiolight_background_blur": 0.0,
|
||||
"scene.display.shading.studiolight_intensity": 0.0,
|
||||
|
||||
@@ -25,6 +25,7 @@ import zipfile
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ import numpy as np
|
||||
import numpy.typing as npt
|
||||
import multiprocessing
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.unit
|
||||
@@ -1237,8 +1238,7 @@ class IfcImporter:
|
||||
if not pset:
|
||||
return
|
||||
if "Aggregate_Index" not in pset.keys():
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
self.file,
|
||||
pset=self.file.by_id(pset["id"]),
|
||||
properties={"Aggregate_Index": aggregate_index, "Name": name},
|
||||
|
||||
@@ -21,6 +21,7 @@ import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.group
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.aggregate as core
|
||||
@@ -126,6 +127,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# Second pass: delete aggregates that now have no parts
|
||||
deleted_aggregates = []
|
||||
for aggregate in aggregates_to_check:
|
||||
# Skip spatial elements - they should not be deleted even if empty
|
||||
if aggregate.is_a("IfcSpatialElement"):
|
||||
continue
|
||||
|
||||
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||
if len(related_objects) == 0:
|
||||
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||
@@ -321,6 +326,21 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
|
||||
# copy selection query to clipboard
|
||||
result = ""
|
||||
for aggregate in all_parts:
|
||||
aggregate_class = aggregate.is_a()
|
||||
aggregate_name = aggregate.Name or ""
|
||||
query = f'parent = "{aggregate_name}"'
|
||||
if not result:
|
||||
result = query
|
||||
else:
|
||||
result += f" + {query}"
|
||||
|
||||
if result:
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
|
||||
self.one_level_deep = False # <-- forcibly reset
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -109,23 +109,6 @@ class BIM_PT_debug(Panel):
|
||||
row.prop(props, "display_type", text="")
|
||||
row.operator("bim.override_display_type").display = props.display_type
|
||||
|
||||
layout.operator("bim.purge_unused_representations")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "ifc_class_purge", text="")
|
||||
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
|
||||
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
|
||||
|
||||
if context.active_object and (data := context.active_object.data):
|
||||
mprops = tool.Geometry.get_mesh_props(data)
|
||||
row = layout.row()
|
||||
row.operator("bim.get_representation_ifc_parameters")
|
||||
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
|
||||
row = layout.row(align=True)
|
||||
row.prop(ifc_parameter, "name", text="")
|
||||
row.prop(ifc_parameter, "value", text="")
|
||||
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
|
||||
|
||||
layout.label(text="Inspector:")
|
||||
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -235,8 +235,8 @@ class DecoratorData:
|
||||
cut_cache = {}
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
camera_location_checksum = None
|
||||
camera_rotation_checksum = None
|
||||
camera_location_checksum = ""
|
||||
camera_rotation_checksum = ""
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import gpu
|
||||
import bpy
|
||||
import blf
|
||||
import os
|
||||
import math
|
||||
import bmesh
|
||||
import shapely
|
||||
@@ -41,7 +42,7 @@ from bonsai.bim.module.drawing.helper import format_distance
|
||||
from timeit import default_timer as timer
|
||||
from functools import cache
|
||||
from typing import Optional, Union
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Generator, Iterator
|
||||
|
||||
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
|
||||
|
||||
@@ -172,15 +173,11 @@ class BaseDecorator:
|
||||
def get_camera_width_mm(self):
|
||||
# Horrific prototype code to ensure bgl draws at drawing scales
|
||||
# https://blender.stackexchange.com/questions/16493/is-there-a-way-to-fit-the-viewport-to-the-current-field-of-view
|
||||
def is_landscape(render):
|
||||
return render.resolution_x > render.resolution_y
|
||||
|
||||
camera = bpy.context.scene.camera
|
||||
render = bpy.context.scene.render
|
||||
if is_landscape(render):
|
||||
camera_width_model = camera.data.ortho_scale
|
||||
else:
|
||||
camera_width_model = camera.data.ortho_scale / render.resolution_y * render.resolution_x
|
||||
|
||||
# Always use the camera's ortho_scale as the width in model space
|
||||
camera_width_model = camera.data.ortho_scale
|
||||
|
||||
scale = tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
|
||||
camera_width_mm = scale * camera_width_model
|
||||
@@ -189,29 +186,26 @@ class BaseDecorator:
|
||||
def camera_zoom_to_factor(self, zoom):
|
||||
return math.pow(((zoom / 50) + math.sqrt(2)) / 2, 2)
|
||||
|
||||
def get_splines(self, obj):
|
||||
def get_splines(self, obj: bpy.types.Object) -> Generator[list[Vector]]:
|
||||
"""Iterates through splines
|
||||
Args:
|
||||
obj: Blender object with Curve data
|
||||
|
||||
Yields:
|
||||
verts: points of each spline, world coords
|
||||
:param obj: Blender object with Curve data
|
||||
:yield: points of each spline, world coords
|
||||
"""
|
||||
assert type(obj.data) is bpy.types.Curve
|
||||
for spline in obj.data.splines:
|
||||
spline_points = spline.bezier_points if spline.bezier_points else spline.points
|
||||
if len(spline_points) < 2:
|
||||
continue
|
||||
yield [obj.matrix_world @ p.co for p in spline_points]
|
||||
|
||||
def get_path_geom(self, obj, topo=True):
|
||||
def get_path_geom(self, obj: bpy.types.Object, topo: bool = True):
|
||||
"""Parses path geometry into line segments
|
||||
|
||||
Args:
|
||||
obj: Blender object with data of type Curve
|
||||
topo: bool; if types of vertices are needed
|
||||
:param obj: Blender object with data of type Curve
|
||||
:param topo: if types of vertices are needed
|
||||
|
||||
Returns:
|
||||
vertices: 3-tuples of coords
|
||||
:return: vertices: 3-tuples of coords
|
||||
indices: 2-tuples of each segment verices' indices
|
||||
topology: types of vertices
|
||||
0: internal
|
||||
@@ -242,11 +236,9 @@ class BaseDecorator:
|
||||
def get_mesh_geom(self, obj, check_mode=True):
|
||||
"""Parses mesh geometry into line segments
|
||||
|
||||
Args:
|
||||
obj: Blender object with data of type Mesh
|
||||
:param obj: Blender object with data of type Mesh
|
||||
|
||||
Returns:
|
||||
vertices: 3-tuples of coords
|
||||
:return: vertices: 3-tuples of coords
|
||||
indices: 2-tuples of each segment verices' indices
|
||||
"""
|
||||
if check_mode and obj.data.is_editmode:
|
||||
@@ -377,11 +369,9 @@ class BaseDecorator:
|
||||
):
|
||||
"""Draw text label
|
||||
|
||||
Args:
|
||||
pos: bottom-center
|
||||
multiline: \n characters will be interpreted as line breaks
|
||||
|
||||
aligned and centered at segment middle
|
||||
:param pos: bottom-center
|
||||
:param multiline: ``\n`` characters will be interpreted as line breaks
|
||||
aligned and centered at segment middle
|
||||
|
||||
NOTE: `blf.draw` seems to ignore the \n character, so we have to split the text ourselves
|
||||
and use the `line_no` argument of `draw_label`
|
||||
@@ -499,7 +489,7 @@ class BaseDecorator:
|
||||
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
|
||||
|
||||
@cache
|
||||
def format_value(self, context, value, custom_unit=None):
|
||||
def format_value(self, context, value, suppress_zero_inches=False, custom_unit=None, in_unit_length=False):
|
||||
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
|
||||
precision = drawing_pset_data.get("MetricPrecision", None)
|
||||
if not precision:
|
||||
@@ -510,8 +500,9 @@ class BaseDecorator:
|
||||
value,
|
||||
precision=precision,
|
||||
decimal_places=decimal_places,
|
||||
suppress_zero_inches=True,
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
custom_unit=custom_unit,
|
||||
in_unit_length=in_unit_length,
|
||||
)
|
||||
|
||||
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
|
||||
@@ -753,7 +744,12 @@ class DimensionDecorator(BaseDecorator):
|
||||
|
||||
if not show_description_only:
|
||||
length = (v1 - v0).length
|
||||
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
|
||||
text = self.format_value(
|
||||
context,
|
||||
length,
|
||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
if isinstance(self, DiameterDecorator):
|
||||
text = "D" + text
|
||||
text = text_prefix + text + text_suffix
|
||||
@@ -1208,7 +1204,11 @@ class PlanLevelDecorator(BaseDecorator):
|
||||
abs_z = verts[0].z
|
||||
z = helper.get_relative_z(obj, element, abs_z)
|
||||
|
||||
rl = self.format_value(context, z)
|
||||
# Z is in meters (Blender world space), convert to project units (feet)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
z = z / unit_scale # Convert meters to feet
|
||||
|
||||
rl = self.format_value(context, z, in_unit_length=True)
|
||||
text = "{}{}".format("" if z < 0 else "+", rl)
|
||||
return text
|
||||
|
||||
@@ -1281,7 +1281,11 @@ class SectionLevelDecorator(BaseDecorator):
|
||||
|
||||
def get_text():
|
||||
z = verts[0].z
|
||||
rl = self.format_value(context, z)
|
||||
# Z is in meters (Blender world space), convert to project units (feet)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
z = z / unit_scale # Convert meters to feet
|
||||
|
||||
rl = self.format_value(context, z, in_unit_length=True)
|
||||
text = "RL {}{}".format("" if z < 0 else "+", rl)
|
||||
return text
|
||||
|
||||
@@ -1641,6 +1645,13 @@ class CutDecorator:
|
||||
if not context.scene.camera:
|
||||
return
|
||||
|
||||
# Check if any viewport is in local view - skip decorations if so
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
space = area.spaces.active
|
||||
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
|
||||
return # Don't draw decorations in local view
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
self.fallback_colour = (0.3, 0.3, 0.3, 1)
|
||||
@@ -1731,21 +1742,38 @@ class CutDecorator:
|
||||
|
||||
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())
|
||||
# Explicit `dtype` for Blender <5.0 compatibility.
|
||||
DecoratorData.camera_location_checksum = repr(
|
||||
np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()
|
||||
)
|
||||
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
|
||||
|
||||
def is_camera_moved(self):
|
||||
if not DecoratorData.camera_location_checksum:
|
||||
self.cache_camera_matrix()
|
||||
return True # Let's be conservative
|
||||
obj = bpy.context.scene.camera
|
||||
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
|
||||
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
loc_checksum_bytes = eval(DecoratorData.camera_location_checksum)
|
||||
if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64)
|
||||
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32)
|
||||
else: # New format: 3 * 4 bytes (float32)
|
||||
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32)
|
||||
|
||||
loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten()
|
||||
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
|
||||
rot_real = np.array(obj.matrix_world.to_3x3())
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
rot_checksum_bytes = eval(DecoratorData.camera_rotation_checksum)
|
||||
if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64)
|
||||
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3)
|
||||
else: # New format: 9 * 4 bytes (float32)
|
||||
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3)
|
||||
|
||||
rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32)
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
if angle_rad > 0.0017453292519943296: # 0.1 degrees
|
||||
@@ -1974,15 +2002,51 @@ class DecorationsHandler:
|
||||
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
|
||||
self.decorators[object_type] = self.decorators["FALL"]
|
||||
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
|
||||
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
if drawing_font := prefs.doc.drawing_font:
|
||||
# Try Blender addon font folder
|
||||
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
|
||||
if drawing_font_path.is_file():
|
||||
font_id = blf.load(drawing_font_path.__str__())
|
||||
for decorator in self.decorators.values():
|
||||
decorator.font_id = font_id
|
||||
|
||||
if not drawing_font_path.is_file():
|
||||
# 2 — Fallback search: Windows font directories
|
||||
# TODO - Linux
|
||||
win_font_dirs = [
|
||||
Path(os.environ.get("WINDIR", "C:\\Windows")) / "Fonts",
|
||||
# Add any custom enterprise paths here if needed
|
||||
]
|
||||
|
||||
found_font = None
|
||||
for font_dir in win_font_dirs:
|
||||
candidate = font_dir / drawing_font
|
||||
if candidate.is_file():
|
||||
found_font = candidate
|
||||
break
|
||||
|
||||
if found_font:
|
||||
drawing_font_path = found_font
|
||||
else:
|
||||
print(f"[BIM] Font '{drawing_font}' not found in addon or Windows fonts.")
|
||||
return # Bail out without assigning fonts
|
||||
|
||||
font_id = blf.load(str(drawing_font_path))
|
||||
|
||||
for decorator in self.decorators.values():
|
||||
decorator.font_id = font_id
|
||||
|
||||
def __call__(self, context):
|
||||
# Check if any viewport is in local view - skip decorations if so
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
space = area.spaces.active
|
||||
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
|
||||
return # Don't draw decorations in local view
|
||||
|
||||
# disable decorations when not in camera view
|
||||
if context.region_data.view_perspective != "CAMERA":
|
||||
return
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import math
|
||||
import ifcopenshell.util.element
|
||||
import mathutils.geometry
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
@@ -121,6 +122,45 @@ class BoundingBox:
|
||||
return retVal
|
||||
|
||||
|
||||
def find_best_precision(fractional_inches, max_precision=256, base_tolerance=0.0001):
|
||||
"""
|
||||
Find the simplest fraction denominator that accurately represents the value.
|
||||
Uses a sliding tolerance - smaller denominators get more tolerance.
|
||||
|
||||
:param fractional_inches: The fractional part of inches (0.0 to 1.0)
|
||||
:param max_precision: Maximum denominator to try (256 for imperial)
|
||||
:param base_tolerance: Base tolerance in inches (very strict)
|
||||
:return: Best denominator (power of 2: 2, 4, 8, 16, 32, 64, 128, 256)
|
||||
"""
|
||||
if abs(fractional_inches) < 0.00001:
|
||||
return 1 # No fraction needed
|
||||
|
||||
# Try denominators from coarsest to finest: 2, 4, 8, 16, 32, 64, 128, 256
|
||||
for exp in range(1, 9): # 2^1 to 2^8
|
||||
denom = 2**exp
|
||||
if denom > max_precision:
|
||||
break
|
||||
|
||||
# Find nearest numerator for this denominator
|
||||
numer = round(fractional_inches * denom)
|
||||
|
||||
# Skip if numerator is 0
|
||||
if numer == 0:
|
||||
continue
|
||||
|
||||
# Check if this fraction is close enough
|
||||
fraction_value = numer / denom
|
||||
error = abs(fraction_value - fractional_inches)
|
||||
|
||||
# Very strict tolerance - only simplify if it's a near-perfect match
|
||||
# This keeps 3/256 as 3/256, but allows floating point errors to be cleaned up
|
||||
if error < base_tolerance:
|
||||
return denom
|
||||
|
||||
# If no simpler fraction works, use max precision
|
||||
return max_precision
|
||||
|
||||
|
||||
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
|
||||
# MeasureIt-ARCH is GPL-v3
|
||||
def format_distance(
|
||||
@@ -186,14 +226,18 @@ def format_distance(
|
||||
if unit_length == "INCHES":
|
||||
toInches = 1
|
||||
if unit_length == "FEET":
|
||||
toInches = 11.9999
|
||||
toInches = 12
|
||||
else:
|
||||
toInches = 39.3700787401574887
|
||||
inPerFoot = 11.9999
|
||||
inPerFoot = 12.0
|
||||
|
||||
if isArea:
|
||||
toInches = 1550
|
||||
inPerFoot = 143.999
|
||||
inPerFoot = 144
|
||||
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
if not precision:
|
||||
precision = 256
|
||||
elif precision == "1":
|
||||
@@ -202,8 +246,6 @@ def format_distance(
|
||||
precision = int(precision.split("/")[1])
|
||||
|
||||
base = int(precision)
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
# Separate ft and inches
|
||||
# Unless Inches are the specified Length Unit or unit_fraction is False
|
||||
@@ -219,20 +261,33 @@ def format_distance(
|
||||
# Separate Fractional Inches
|
||||
decInches = abs(decInches) # ignore the sign for inches
|
||||
inches = math.floor(decInches) # remove decimal
|
||||
if inches != 0:
|
||||
frac = round(base * (decInches - inches))
|
||||
|
||||
# Clean up floating point errors
|
||||
fractional_inches = decInches - inches
|
||||
tolerance = 0.01 # About 1/100 of an inch
|
||||
|
||||
# If the fractional part is very small, treat it as zero
|
||||
if fractional_inches < tolerance:
|
||||
frac = 0
|
||||
# If very close to the next whole inch, round up
|
||||
elif fractional_inches > (1.0 - tolerance):
|
||||
frac = 0
|
||||
inches += 1
|
||||
else:
|
||||
frac = round(base * (decInches))
|
||||
# Calculate fraction normally
|
||||
if inches != 0:
|
||||
frac = round(base * fractional_inches)
|
||||
else:
|
||||
frac = round(base * fractional_inches)
|
||||
|
||||
# Set proper numerator and denominator
|
||||
if frac != base:
|
||||
numcycles = int(math.log2(base))
|
||||
for i in range(numcycles):
|
||||
if frac % 2 == 0:
|
||||
frac = int(frac / 2)
|
||||
base = int(base / 2)
|
||||
else:
|
||||
break
|
||||
# Simplify using GCD
|
||||
from math import gcd
|
||||
|
||||
divisor = gcd(int(frac), int(base))
|
||||
frac = int(frac / divisor)
|
||||
base = int(base / divisor)
|
||||
else:
|
||||
frac = 0
|
||||
inches += 1
|
||||
@@ -251,31 +306,46 @@ def format_distance(
|
||||
frac = None
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
|
||||
|
||||
tx_dist = ""
|
||||
if feet:
|
||||
tx_dist += str(feet) + "'"
|
||||
if not feet and not add_inches:
|
||||
tx_dist += str(feet) + "'"
|
||||
if feet and add_inches:
|
||||
|
||||
# Add "0' - " when we have inches but no feet
|
||||
# But only add " - " separator if we actually have inches to show
|
||||
if not feet and add_inches:
|
||||
tx_dist += "0' - "
|
||||
elif feet and add_inches:
|
||||
tx_dist += " - "
|
||||
|
||||
if not feet and value < 0:
|
||||
tx_dist += "-"
|
||||
if add_inches:
|
||||
if feet == 0 and inches == 0:
|
||||
pass
|
||||
if feet == 0 and inches == 0 and not frac:
|
||||
# Special case: exactly zero, show "0"
|
||||
tx_dist += "0"
|
||||
elif feet == 0 and inches == 0:
|
||||
pass # Has fraction, will be added below
|
||||
else:
|
||||
tx_dist += str(inches)
|
||||
if add_inches and frac:
|
||||
if feet == 0 and inches == 0:
|
||||
pass
|
||||
pass # Has fraction, will be added below
|
||||
else:
|
||||
tx_dist += " "
|
||||
if frac:
|
||||
tx_dist += str(frac) + "/" + str(base)
|
||||
if add_inches or frac:
|
||||
tx_dist += '"'
|
||||
# Only add inch symbol if we actually added inch content
|
||||
if inches > 0 or frac > 0 or feet == 0:
|
||||
tx_dist += '"'
|
||||
|
||||
if precision == "12" and unit_system == "IMPERIAL":
|
||||
tx_dist = str(round(decFeet)) + "'"
|
||||
if tx_dist == '"':
|
||||
tx_dist = "0' - 0\""
|
||||
else:
|
||||
fmt = "%1.3f"
|
||||
sq_feet = round(value * toInches / inPerFoot, 4)
|
||||
@@ -421,8 +491,8 @@ def ortho_view_frame(
|
||||
|
||||
Similar to `bpy.types.Camera.view_frame`
|
||||
|
||||
:arg camera: camera of drawing
|
||||
:arg margin: margins, in scene units
|
||||
:param camera: camera of drawing
|
||||
:param margin: margins, in scene units
|
||||
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
|
||||
"""
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
@@ -443,8 +513,8 @@ def almost_zero(v):
|
||||
def clip_segment(bounds, segm):
|
||||
"""Clipping line segment to bounds
|
||||
|
||||
:arg bounds: (xmin, xmax, ymin, ymax)
|
||||
:arg segm: 2 vertices of the segment
|
||||
:param bounds: (xmin, xmax, ymin, ymax)
|
||||
:param segm: 2 vertices of the segment
|
||||
:return: 2 new vertices of segment or None if segment outside the bounding box
|
||||
"""
|
||||
# Liang–Barsky algorithm
|
||||
@@ -493,8 +563,8 @@ def clip_segment(bounds, segm):
|
||||
def elevate_segment(bounds, segm):
|
||||
"""Elevate line xy-perpendicular segment vertically
|
||||
|
||||
:arg bounds: (xmin, xmax, ymin, ymax)
|
||||
:arg segm: 2 vertices of the segment
|
||||
:param bounds: (xmin, xmax, ymin, ymax)
|
||||
:param segm: 2 vertices of the segment
|
||||
:return: 2 new vertices of segment or None if segment outside the bounding box
|
||||
"""
|
||||
_, _, ymin, ymax, zmin, _ = bounds
|
||||
@@ -544,13 +614,11 @@ def add_newline_between_words(text: str, newline_at: int) -> str:
|
||||
def get_relative_z(obj: bpy.types.Object, element, abs_z: float) -> float:
|
||||
"""Return relative Z of an element, accounting for its spatial container.
|
||||
|
||||
Args:
|
||||
obj: The Blender object representing the element.
|
||||
element: The IFC entity for the object.
|
||||
abs_z: The absolute Z value in world coordinates.
|
||||
:param obj: The Blender object representing the element.
|
||||
:param element: The IFC entity for the object.
|
||||
:param abs_z: The absolute Z value in world coordinates.
|
||||
|
||||
Returns:
|
||||
Relative Z value if the element is inside a spatial container,
|
||||
:return: Relative Z value if the element is inside a spatial container,
|
||||
otherwise the absolute Z.
|
||||
"""
|
||||
z = abs_z
|
||||
|
||||
@@ -932,6 +932,31 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
return svg_path
|
||||
|
||||
# Add target_view and scale classes to the parent group from IFC data
|
||||
if group is not None:
|
||||
existing_classes = group.get("class", "").split()
|
||||
|
||||
# Add target_view class
|
||||
if hasattr(self, "cprops") and getattr(self.cprops, "target_view", None):
|
||||
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
|
||||
target_view_full_class = f"target-view-{target_view_class}"
|
||||
if target_view_full_class not in existing_classes:
|
||||
existing_classes.append(target_view_full_class)
|
||||
|
||||
# Add scale class from EPset_Drawing.Scale
|
||||
drawing_pset = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing")
|
||||
if drawing_pset and drawing_pset.get("Scale"):
|
||||
scale_value = drawing_pset["Scale"]
|
||||
# Remove "1/" prefix if it exists
|
||||
if isinstance(scale_value, str) and scale_value.startswith("1/"):
|
||||
scale_value = scale_value[2:]
|
||||
scale_class = tool.Drawing.canonicalise_class_name(str(scale_value))
|
||||
scale_full_class = f"scale-{scale_class}"
|
||||
if scale_full_class not in existing_classes:
|
||||
existing_classes.append(scale_full_class)
|
||||
|
||||
group.set("class", " ".join(existing_classes))
|
||||
|
||||
if self.cprops.cut_mode == "BISECT":
|
||||
self.remove_cut_linework(root)
|
||||
self.generate_bisect_linework(context, root)
|
||||
@@ -1287,11 +1312,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
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:
|
||||
@@ -2701,21 +2721,59 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def set_raster_style(self, context: bpy.types.Context) -> None:
|
||||
scene = context.scene # Do not remove. It is used in exec later
|
||||
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used in exec later
|
||||
style = json.loads(self.drawing_style.raster_style)
|
||||
style: dict[str, Any] = json.loads(self.drawing_style.raster_style)
|
||||
|
||||
VIEWPORT_SHADING_TYPE = "scene.display.shading.type"
|
||||
|
||||
def preprocess(path: str, value: Any) -> tuple[str, Any, bool, bool]:
|
||||
warning = False
|
||||
skip = False
|
||||
# @25.05.12
|
||||
if path == "scene.render.engine" and value == "BLENDER_EEVEE":
|
||||
value = "BLENDER_EEVEE_NEXT"
|
||||
|
||||
BLENDER_5_REMOVED = (
|
||||
"scene.render.bake_bias",
|
||||
"scene.render.bake_margin",
|
||||
"scene.render.bake_margin_type",
|
||||
"scene.render.bake_samples",
|
||||
"scene.render.bake_type",
|
||||
"scene.render.bake_user_scale",
|
||||
"scene.render.use_bake_clear",
|
||||
"scene.render.use_bake_lores_mesh",
|
||||
"scene.render.use_bake_multires",
|
||||
"scene.render.use_bake_selected_to_active",
|
||||
"scene.render.use_bake_user_scale",
|
||||
)
|
||||
|
||||
# 25.11.07, Blender 5+
|
||||
if path == "scene.render.engine" and value in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
|
||||
if value == "BLENDER_EEVEE_NEXT":
|
||||
print(
|
||||
f"Warning: Value 'BLENDER_EEVEE_NEXT' is outdated for property '{path}' "
|
||||
"since Blender 5.0 and should be replaced with 'BLENDER_EEVEE' in shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
value = tool.Blender.get_eevee_name()
|
||||
elif tool.Blender.BLENDER_5 and path in BLENDER_5_REMOVED:
|
||||
print(
|
||||
f"Warning: Value 'BLENDER_EEVEE' is outdated for property '{path}' "
|
||||
"since Blender 4.2 and should be replaced with 'BLENDER_EEVEE_NEXT' in shading_styles.json."
|
||||
f"Warning: Property '{path}' is removed "
|
||||
"since Blender 5.0 and should be also removed from shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
skip = True
|
||||
# @25.11.11
|
||||
elif (
|
||||
path == "scene.display.shading.studio_light"
|
||||
and value == "Default"
|
||||
and style[VIEWPORT_SHADING_TYPE] in ("RENDERED", "MATERIAL")
|
||||
):
|
||||
value = "forest.exr"
|
||||
print(
|
||||
f"Warning: Value 'Default' for property '{path}' and "
|
||||
f"'{VIEWPORT_SHADING_TYPE}' = '{style[VIEWPORT_SHADING_TYPE]}' is outdated "
|
||||
"and should be replaced with 'forest.exr' in shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
# @25.05.12
|
||||
if path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
|
||||
elif path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
|
||||
value = "THEME"
|
||||
print(
|
||||
f"Warning: Value 'MATERIAL' is outdated for property '{path}' "
|
||||
@@ -2749,7 +2807,16 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
return path, value, warning, skip
|
||||
|
||||
for path, value in style.items():
|
||||
paths = list(style.keys())
|
||||
PRIORITY_PATHS = (
|
||||
# `scene.display.shading.studio_light` values depend on `.type`
|
||||
# so we got to set it first.
|
||||
VIEWPORT_SHADING_TYPE,
|
||||
)
|
||||
paths.sort(key=lambda p: p not in PRIORITY_PATHS)
|
||||
|
||||
for path in paths:
|
||||
value = style[path]
|
||||
path, value, warning, skip = preprocess(path, value)
|
||||
self.has_warnings_during_activation |= warning
|
||||
|
||||
@@ -3414,6 +3481,15 @@ class LoadDrawings(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Operator is not accessible through UI if IFC project is not saved,
|
||||
# but adding poll check to avoid using Drawings UI in scripts with unsaved project.
|
||||
if tool.Ifc.get_path():
|
||||
return True
|
||||
cls.poll_message_set("IFC project is not saved.")
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
core.load_drawings(tool.Drawing)
|
||||
|
||||
|
||||
@@ -544,11 +544,10 @@ class Scheduler:
|
||||
"""
|
||||
Adds text to svg.
|
||||
|
||||
Args:
|
||||
p_tags: list of cell's P tags from odt file
|
||||
box_alignment: alignment of text in box
|
||||
wrap_text: if True, text will be wrapped to fit in cell
|
||||
cell_width: width of cell, used for wrapping text
|
||||
:param p_tags: list of cell's P tags from odt file
|
||||
:param box_alignment: alignment of text in box
|
||||
:param wrap_text: if True, text will be wrapped to fit in cell
|
||||
:param cell_width: width of cell, used for wrapping text
|
||||
"""
|
||||
text_lines = [str(p) for p in p_tags]
|
||||
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
|
||||
|
||||
@@ -341,17 +341,29 @@ class SheetBuilder:
|
||||
assert style_data is not None
|
||||
text = ""
|
||||
brackets_level = 0
|
||||
selector_buffer = "" # Buffer to accumulate selectors across lines
|
||||
|
||||
for l in style_data:
|
||||
if l == "{":
|
||||
if brackets_level == 0:
|
||||
cur_line = text.splitlines()[-1]
|
||||
text = text[: -len(cur_line)]
|
||||
# Get all accumulated selector text (may span multiple lines)
|
||||
# Find where the last rule ended (after last }) or start of text
|
||||
last_close = text.rfind("}")
|
||||
if last_close == -1:
|
||||
selector_text = text
|
||||
text = ""
|
||||
else:
|
||||
selector_text = text[last_close + 1 :]
|
||||
text = text[: last_close + 1]
|
||||
|
||||
# Process all selectors (split by comma)
|
||||
css_selectors = []
|
||||
# making sure cases like "text, tspan" will be
|
||||
# converted to "text.prefix, tspan.prefix"
|
||||
for css_selector in cur_line.split(","):
|
||||
css_selector = f"{css_selector.strip()}.{prefix}"
|
||||
css_selectors.append(css_selector)
|
||||
for css_selector in selector_text.split(","):
|
||||
css_selector = css_selector.strip()
|
||||
if css_selector: # Only process non-empty selectors
|
||||
css_selector = f"{css_selector}.{prefix}"
|
||||
css_selectors.append(css_selector)
|
||||
|
||||
text += ", ".join(css_selectors) + " "
|
||||
brackets_level += 1
|
||||
elif l == "}":
|
||||
|
||||
@@ -202,6 +202,15 @@ class SvgWriter:
|
||||
) -> Self:
|
||||
self.precision = precision
|
||||
self.decimal_places = decimal_places
|
||||
|
||||
# Sort annotations to ensure FILLAREA types are drawn first (at the bottom)
|
||||
def sort_key(element):
|
||||
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
# Return 0 for FILLAREA to draw them first, 1 for everything else
|
||||
return 0 if predefined_type == "FILL_AREA" else 1
|
||||
|
||||
annotations = sorted(annotations, key=sort_key)
|
||||
|
||||
for element in annotations:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if (
|
||||
|
||||
@@ -116,7 +116,7 @@ class BIM_PT_camera(Panel):
|
||||
# See #6686.
|
||||
if (
|
||||
props.has_underlay
|
||||
and str(render.engine) == "BLENDER_EEVEE_NEXT"
|
||||
and str(render.engine) == tool.Blender.get_eevee_name()
|
||||
and ((megapixels := (render.resolution_x * render.resolution_y / 10**6)) > MEGAPIXELS_WARNING_THRESHOLD)
|
||||
):
|
||||
box = self.layout.box()
|
||||
|
||||
@@ -30,6 +30,7 @@ classes = (
|
||||
operator.AddSweptAreaSolidItem,
|
||||
operator.AssignRepresentationLayer,
|
||||
operator.CopyRepresentation,
|
||||
operator.DirectProfileEdit,
|
||||
operator.DisableEditingRepresentationItemShapeAspect,
|
||||
operator.DisableEditingRepresentationItemStyle,
|
||||
operator.DisableEditingRepresentationItems,
|
||||
@@ -160,6 +161,8 @@ def register():
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
# Deletion.
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
@@ -178,6 +181,8 @@ def register():
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
kmi.properties.should_save = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
|
||||
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
@@ -42,6 +42,7 @@ import bonsai.core.geometry
|
||||
import bonsai.core.geometry as core
|
||||
import bonsai.core.aggregate
|
||||
import bonsai.core.nest
|
||||
import bonsai.core.spatial
|
||||
import bonsai.core.style
|
||||
import bonsai.core.root
|
||||
import bonsai.core.drawing
|
||||
@@ -809,9 +810,17 @@ class OverrideDelete(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_objects) > 0
|
||||
# Match `object.delete` poll for consistency.
|
||||
# `object.delete` poll just checks for OBJECT mode.
|
||||
poll = bpy.ops.object.delete.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
# Deep magick from the dawn of time
|
||||
if tool.Ifc.get() is None:
|
||||
bpy.ops.object.delete(use_global=self.use_global, confirm=self.confirm)
|
||||
@@ -823,6 +832,8 @@ class OverrideDelete(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
assert context.window_manager
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file is None:
|
||||
@@ -964,6 +975,10 @@ class OverrideDelete(bpy.types.Operator):
|
||||
except:
|
||||
continue
|
||||
|
||||
# Skip spatial elements - they should not be deleted even if empty
|
||||
if aggregate.is_a("IfcSpatialElement"):
|
||||
continue
|
||||
|
||||
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||
if len(related_objects) == 0:
|
||||
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||
@@ -1030,9 +1045,18 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context) -> bool:
|
||||
return len(getattr(context, "selected_ids", [])) > 0
|
||||
# Match `outliner.delete` poll for consistency.
|
||||
# `outliner.delete` just checks `area.type` == `OUTLINER`.
|
||||
poll = bpy.ops.outliner.delete.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available from Outliner.")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if len(getattr(context, "selected_ids", [])) == 0:
|
||||
return {"FINISHED"}
|
||||
|
||||
# In this override, we don't check self.hierarchy. This effectively
|
||||
# makes Delete and Delete Hierarchy identical. This is on purpose, since
|
||||
# non-hierarchical deletion may imply a whole bunch of potentially
|
||||
@@ -1051,6 +1075,9 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if len(getattr(context, "selected_ids", [])) == 0:
|
||||
return {"FINISHED"}
|
||||
|
||||
assert context.window_manager
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file:
|
||||
@@ -1121,10 +1148,18 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_objects) > 0
|
||||
def poll(cls, context) -> bool:
|
||||
# Match `object.duplicate_move` poll for consistency.
|
||||
# `object.duplicate_move` poll checks for OBJECT mode.
|
||||
poll = bpy.ops.object.duplicate_move.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -1147,7 +1182,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False):
|
||||
objects_to_remove = set()
|
||||
objects_to_remove: set[bpy.types.Object] = set()
|
||||
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -1205,6 +1240,9 @@ class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
OldToNewType = dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
|
||||
|
||||
|
||||
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.object_duplicate_move_linked_aggregate"
|
||||
bl_label = "IFC Duplicate and Move Linked Aggregate"
|
||||
@@ -1227,9 +1265,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
self.new_active_obj = None
|
||||
self.group_name = "BBIM_Linked_Aggregate"
|
||||
self.pset_name = "BBIM_Linked_Aggregate"
|
||||
old_to_new = {}
|
||||
all_old_to_new = {} # Track all duplicates created
|
||||
|
||||
def select_objects_and_add_data(element):
|
||||
def select_objects_and_add_data(element: ifcopenshell.entity_instance) -> None:
|
||||
add_linked_aggregate_group(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj.select_set(True)
|
||||
@@ -1243,8 +1281,6 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
select_objects_and_add_data(part)
|
||||
else:
|
||||
index = add_linked_aggregate_pset(part, index)
|
||||
# index += 1
|
||||
|
||||
obj = tool.Ifc.get_object(part)
|
||||
obj.select_set(True)
|
||||
|
||||
@@ -1257,20 +1293,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name=self.pset_name)
|
||||
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
index += 1
|
||||
else:
|
||||
pass
|
||||
|
||||
return index
|
||||
|
||||
def add_linked_aggregate_group(element):
|
||||
def add_linked_aggregate_group(element: ifcopenshell.entity_instance) -> None:
|
||||
linked_aggregate_group = None
|
||||
product_groups_name = [
|
||||
r.RelatingGroup.Name
|
||||
@@ -1283,25 +1314,21 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
linked_aggregate_group = ifcopenshell.api.group.add_group(ifc_file, name=self.group_name)
|
||||
ifcopenshell.api.group.assign_group(ifc_file, products=[element], group=linked_aggregate_group)
|
||||
|
||||
def custom_incremental_naming_for_element_assembly(old_to_new):
|
||||
def custom_incremental_naming_for_element_assembly(old_to_new: OldToNewType) -> None:
|
||||
for new in old_to_new.values():
|
||||
if new[0].is_a("IfcElementAssembly"):
|
||||
group_elements: list[ifcopenshell.entity_instance] = next(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
)
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
new_obj.name = pset["Name"] + "_" + str(pset["Aggregate_Index"])
|
||||
new_obj.name = f"{pset['Name']}_{pset['Aggregate_Index']:02d}"
|
||||
|
||||
def get_max_index(parts):
|
||||
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
|
||||
index = [i["Index"] for i in psets if i]
|
||||
if len(index) > 0:
|
||||
index = max(index)
|
||||
psets = [
|
||||
ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
|
||||
for p in parts
|
||||
if ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
|
||||
]
|
||||
if psets:
|
||||
index = max([i["Index"] for i in psets if i])
|
||||
return index
|
||||
else:
|
||||
return 0
|
||||
@@ -1315,23 +1342,28 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
|
||||
|
||||
group_elements: list[ifcopenshell.entity_instance] = next(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
)
|
||||
if linked_aggregate_group:
|
||||
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
|
||||
if pset:
|
||||
new_pset = ifcopenshell.api.pset.add_pset(ifc_file, product=new[0], name=self.pset_name)
|
||||
if pset["Index"] == 0:
|
||||
group_elements = []
|
||||
if new[0].is_a("IfcElementAssembly"):
|
||||
group_elements = next(
|
||||
(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
and "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
),
|
||||
[],
|
||||
)
|
||||
properties = {
|
||||
"Index": pset["Index"],
|
||||
"Name": pset["Name"],
|
||||
"Aggregate_Index": len(group_elements) - 1,
|
||||
"Aggregate_Index": len(group_elements) - 1 if group_elements else 0,
|
||||
}
|
||||
else:
|
||||
properties = {"Index": pset["Index"]}
|
||||
@@ -1351,43 +1383,73 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
location_diff = new_obj.location - base_obj_location
|
||||
new_obj.location = context.scene.cursor.location + location_diff
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
# Process multiple selected aggregates
|
||||
selected_aggregates = []
|
||||
for obj in context.selected_objects:
|
||||
selected_element = tool.Ifc.get_entity(obj)
|
||||
if not selected_element:
|
||||
continue
|
||||
|
||||
# Find the aggregate element
|
||||
if selected_element.is_a("IfcElementAssembly"):
|
||||
selected_aggregates.append(selected_element)
|
||||
elif selected_element.Decomposes:
|
||||
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_aggregates.append(selected_element.Decomposes[0].RelatingObject)
|
||||
|
||||
# Remove duplicates
|
||||
selected_aggregates = list(set(selected_aggregates))
|
||||
|
||||
if not selected_aggregates:
|
||||
self.report({"INFO"}, "No Linked Aggregates selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
selected_obj = context.selected_objects[0]
|
||||
selected_element = tool.Ifc.get_entity(selected_obj)
|
||||
assert selected_element
|
||||
|
||||
if selected_element.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
elif selected_element.Decomposes:
|
||||
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_element = selected_element.Decomposes[0].RelatingObject
|
||||
selected_obj = tool.Ifc.get_object(selected_element)
|
||||
else:
|
||||
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
|
||||
return {"FINISHED"}
|
||||
|
||||
select_objects_and_add_data(selected_element)
|
||||
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
copy_linked_aggregate_data(old_to_new)
|
||||
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
if location_from_3d_cursor:
|
||||
get_location_from_3d_cursor(old_to_new, selected_element)
|
||||
|
||||
# Deselect all first
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
new_aggregate = old_to_new[selected_element][0]
|
||||
tool.Blender.set_active_object(tool.Ifc.get_object(new_aggregate))
|
||||
|
||||
# Process each selected aggregate
|
||||
for aggregate in selected_aggregates:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
|
||||
# Select and prepare the aggregate for duplication
|
||||
select_objects_and_add_data(aggregate)
|
||||
|
||||
# Duplicate the aggregate
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
all_old_to_new.update(old_to_new) # Collect all duplicates
|
||||
|
||||
# Recreate aggregate structure
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
# Copy linked aggregate data
|
||||
copy_linked_aggregate_data(old_to_new)
|
||||
|
||||
# Apply custom naming
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
# Apply 3D cursor location if requested
|
||||
if location_from_3d_cursor:
|
||||
get_location_from_3d_cursor(old_to_new, aggregate)
|
||||
|
||||
# Deselect for next iteration
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
# Select all newly created aggregates
|
||||
for aggregate in selected_aggregates:
|
||||
if aggregate in all_old_to_new:
|
||||
new_aggregate = all_old_to_new[aggregate][0]
|
||||
new_aggregate_obj = tool.Ifc.get_object(new_aggregate)
|
||||
if new_aggregate_obj:
|
||||
new_aggregate_obj.select_set(True)
|
||||
|
||||
# Set active object to the first new aggregate
|
||||
if all_old_to_new:
|
||||
first_new_aggregate = next(iter(all_old_to_new.values()))[0]
|
||||
context.view_layer.objects.active = tool.Ifc.get_object(first_new_aggregate)
|
||||
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
return old_to_new
|
||||
return all_old_to_new
|
||||
|
||||
|
||||
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
|
||||
@@ -1423,7 +1485,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.pset_name = "BBIM_Linked_Aggregate"
|
||||
refresh_start_time = time()
|
||||
old_to_new = {}
|
||||
original_data: dict[int, dict[int, str]] = {}
|
||||
original_data: dict[int, dict[int, dict[str, Any]]] = {}
|
||||
|
||||
def delete_objects(element: ifcopenshell.entity_instance) -> None:
|
||||
"""Remove IfcElementAssembly and it's parts."""
|
||||
@@ -1469,8 +1531,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]:
|
||||
group = next(
|
||||
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, dict[str, Any]]]:
|
||||
group: int = next(
|
||||
r.RelatingGroup
|
||||
for r in getattr(element, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
@@ -1478,8 +1540,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
).id()
|
||||
original_data[group] = {}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset["Index"]
|
||||
pset: dict[str, Any] = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index: int = pset["Index"]
|
||||
annotations = get_assignments(element)
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
original_data[group][index] = {
|
||||
@@ -1493,6 +1555,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if parts:
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
# TODO: unused expression.
|
||||
original_data | get_original_data(part)
|
||||
else:
|
||||
try:
|
||||
@@ -2346,6 +2409,14 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
item = tool.Geometry.get_active_representation(obj)
|
||||
assert item
|
||||
|
||||
# Fix vertex order for annotation items
|
||||
rep_obj = props.representation_obj
|
||||
if rep_obj:
|
||||
element = tool.Ifc.get_entity(rep_obj)
|
||||
if element and self._is_annotation_object(element):
|
||||
tool.Geometry.ensure_annotation_vertex_order(obj)
|
||||
|
||||
if tool.Geometry.is_meshlike_item(item):
|
||||
if tool.Geometry.is_geometric_data(obj.data) and (
|
||||
item.is_a("IfcVertex") or item.is_a("IfcEdge") or obj.data.polygons
|
||||
@@ -2485,6 +2556,289 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.mode = "EDIT"
|
||||
props.is_changing_mode = False
|
||||
|
||||
def _is_annotation_object(self, element: ifcopenshell.entity_instance) -> bool:
|
||||
"""Check if element is an annotation object that needs vertex order preservation."""
|
||||
if not element.is_a("IfcAnnotation"):
|
||||
return False
|
||||
|
||||
# Object types that have semantic vertex order (arrow direction, text position)
|
||||
annotation_types_with_order = {
|
||||
"TEXT_LEADER",
|
||||
"DIMENSION",
|
||||
"RADIUS",
|
||||
"DIAMETER",
|
||||
"ANGLE",
|
||||
"FALL",
|
||||
"SLOPE_ANGLE",
|
||||
"SLOPE_FRACTION",
|
||||
"SLOPE_PERCENT",
|
||||
"STAIR_ARROW",
|
||||
"PLAN_LEVEL",
|
||||
"SECTION_LEVEL",
|
||||
"SECTION",
|
||||
"ELEVATION",
|
||||
}
|
||||
|
||||
return element.ObjectType in annotation_types_with_order
|
||||
|
||||
|
||||
class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.direct_profile_edit"
|
||||
bl_label = "IFC Direct Profile Edit"
|
||||
bl_description = "Directly enter profile/axis edit mode, or exit back to object mode"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Only allow in OBJECT or EDIT modes
|
||||
if context.mode not in ("OBJECT", "EDIT_MESH", "EDIT_CURVE"):
|
||||
return False
|
||||
|
||||
# Must have an active object
|
||||
if not context.active_object:
|
||||
return False
|
||||
|
||||
# In edit mode, we're good to go (we'll exit)
|
||||
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
|
||||
return True
|
||||
|
||||
# In object mode, check if it's an editable object type
|
||||
obj = context.active_object
|
||||
|
||||
# Only mesh and curve objects are editable
|
||||
if obj.type not in ("MESH", "CURVE"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
# Ensure we're only working with the active object
|
||||
# Deselect all other objects if multiple are selected
|
||||
active_obj = context.active_object
|
||||
if active_obj and context.mode == "OBJECT":
|
||||
if len(context.selected_objects) > 1:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Currently Direct Profile Edit only works on one object at a time. Operating on active object only.",
|
||||
)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
active_obj.select_set(True)
|
||||
context.view_layer.objects.active = active_obj
|
||||
|
||||
# If we're already in edit mode, exit to object mode and SAVE changes
|
||||
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
|
||||
return self.handle_exit_edit_mode(context)
|
||||
|
||||
# We're in object mode, try to enter profile/axis edit
|
||||
return self.handle_enter_edit_mode(context)
|
||||
|
||||
def handle_exit_edit_mode(self, context):
|
||||
"""Handle exiting from edit mode and saving changes."""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if we're editing a representation item
|
||||
if tool.Geometry.is_representation_item(obj):
|
||||
return self.exit_item_edit_mode(context, obj)
|
||||
|
||||
# Check if we're editing an element-level profile or axis
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and tool.Geometry.has_mesh_properties(obj.data):
|
||||
return self.exit_element_edit_mode(context, obj, element)
|
||||
|
||||
# For other edit modes, use standard operator
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
def exit_item_edit_mode(self, context, obj):
|
||||
"""Exit from SweptAreaSolid item editing."""
|
||||
try:
|
||||
item = tool.Geometry.get_active_representation(obj)
|
||||
if not item or not item.is_a("IfcSweptAreaSolid"):
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
# Use the standard save workflow for items
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
ProfileDecorator.uninstall()
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
|
||||
profile = tool.Model.export_profile(obj)
|
||||
if not profile:
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE")
|
||||
|
||||
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
ProfileDecorator.install(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
old_profile = item.SweptArea
|
||||
profile.ProfileName = old_profile.ProfileName
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
for inverse in ifc_file.get_inverse(old_profile):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
|
||||
|
||||
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, old_profile)
|
||||
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
if props.representation_obj:
|
||||
tool.Geometry.reload_representation(props.representation_obj)
|
||||
tool.Geometry.disable_item_mode()
|
||||
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to save item changes: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def exit_element_edit_mode(self, context, obj, element):
|
||||
"""Exit from element-level profile or axis editing."""
|
||||
try:
|
||||
mesh_props = tool.Geometry.get_mesh_props(obj.data)
|
||||
|
||||
# Check if we're editing axis
|
||||
if mesh_props.subshape_type == "AXIS":
|
||||
return bpy.ops.bim.edit_extrusion_axis()
|
||||
|
||||
# Otherwise, we're editing a profile
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if extrusion:
|
||||
return bpy.ops.bim.edit_extrusion_profile()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to save profile/axis changes: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Fallback to standard operator
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
def handle_enter_edit_mode(self, context):
|
||||
"""Handle entering edit mode from object mode."""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if object has mesh or curve data
|
||||
if not hasattr(obj, "data") or not obj.data:
|
||||
self.report({"INFO"}, "Object has no editable data")
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
self.report({"INFO"}, "Active object is not an IFC element")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if this is a LAYER2 element (wall, railing, etc.)
|
||||
try:
|
||||
material_usage = tool.Model.get_usage_type(element)
|
||||
except:
|
||||
material_usage = None
|
||||
|
||||
if material_usage == "LAYER2":
|
||||
self.report({"ERROR"}, "LAYER2 elements (walls, railings, etc.) cannot use profile editing.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Try to get representation
|
||||
try:
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
except:
|
||||
representation = None
|
||||
|
||||
if not representation:
|
||||
self.report({"INFO"}, "Object has no active representation")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check for PROFILE usage (beams, columns, members) - should edit axis
|
||||
if material_usage == "PROFILE":
|
||||
try:
|
||||
return bpy.ops.bim.enable_editing_extrusion_axis()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enable axis editing: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if this is an element with an extrusion profile (LAYER3, etc)
|
||||
try:
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if extrusion and not tool.Geometry.is_representation_item(obj):
|
||||
return bpy.ops.bim.enable_editing_extrusion_profile()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enable profile editing: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Otherwise, try to enter item mode and edit SweptAreaSolid
|
||||
return self.enter_item_edit_mode(context, obj)
|
||||
|
||||
def enter_item_edit_mode(self, context, obj):
|
||||
"""Enter item mode for SweptAreaSolid editing."""
|
||||
try:
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
if not props.representation_obj:
|
||||
bpy.ops.bim.import_representation_items()
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
|
||||
# Find the SweptAreaSolid item
|
||||
swept_solid_obj = None
|
||||
for item_obj_data in props.item_objs:
|
||||
item_obj = item_obj_data.obj
|
||||
if not item_obj:
|
||||
continue
|
||||
try:
|
||||
item = tool.Geometry.get_active_representation(item_obj)
|
||||
if item and item.is_a("IfcSweptAreaSolid"):
|
||||
swept_solid_obj = item_obj
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not swept_solid_obj:
|
||||
self.report({"INFO"}, "No editable SweptAreaSolid item found")
|
||||
tool.Geometry.disable_item_mode()
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Select and activate the swept solid object
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
swept_solid_obj.select_set(True)
|
||||
context.view_layer.objects.active = swept_solid_obj
|
||||
|
||||
# Import the profile to prepare for editing
|
||||
item = tool.Geometry.get_active_representation(swept_solid_obj)
|
||||
if item and item.is_a("IfcSweptAreaSolid"):
|
||||
profile = item.SweptArea
|
||||
res = tool.Model.import_profile(profile, obj=swept_solid_obj)
|
||||
if res is None:
|
||||
self.report({"ERROR"}, "Couldn't import profile for editing")
|
||||
tool.Geometry.disable_item_mode()
|
||||
return {"CANCELLED"}
|
||||
|
||||
tool.Ifc.link(item, swept_solid_obj.data)
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
ProfileDecorator.install(context)
|
||||
|
||||
if not bpy.app.background:
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enter item edit mode: {str(e)}")
|
||||
try:
|
||||
tool.Geometry.disable_item_mode()
|
||||
except:
|
||||
pass
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class FlipObject(bpy.types.Operator):
|
||||
bl_idname = "bim.flip_object"
|
||||
@@ -3053,6 +3407,17 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
tool.Root.reload_item_decorator()
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.is_a("IfcAnnotation") and element.ObjectType in {
|
||||
"TEXT_LEADER", "DIMENSION", "RADIUS", "DIAMETER", "ANGLE",
|
||||
"FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT",
|
||||
"STAIR_ARROW", "PLAN_LEVEL", "SECTION_LEVEL", "SECTION", "ELEVATION"
|
||||
}:
|
||||
for item_obj_data in props.item_objs:
|
||||
item_obj = item_obj_data.obj
|
||||
if item_obj and isinstance(item_obj.data, bpy.types.Mesh) and item_obj.data.vertices:
|
||||
item_obj.data["bonsai_first_vert_co"] = item_obj.data.vertices[0].co[:]
|
||||
|
||||
|
||||
class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.update_item_attributes"
|
||||
|
||||
@@ -96,11 +96,12 @@ def update_shadow_mode(self: "BIMSolarProperties", context: bpy.types.Context) -
|
||||
sun_props = tool.Blender.get_sun_props()
|
||||
assert sun_props
|
||||
if sun_props.sun_object is None:
|
||||
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
|
||||
bpy.ops.object.move_to_collection(collection_index=0)
|
||||
sun_props.sun_object = bpy.context.active_object
|
||||
light = bpy.data.lights.new(name="Sun", type="SUN")
|
||||
sun = bpy.data.objects.new("Sun", light)
|
||||
context.scene.collection.objects.link(sun)
|
||||
sun_props.sun_object = sun
|
||||
update_sun_path(self)
|
||||
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
|
||||
context.scene.render.engine = tool.Blender.get_eevee_name()
|
||||
assert context.scene.display
|
||||
assert context.scene.display.shading
|
||||
context.scene.display.shading.light = "FLAT"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import calendar
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -192,24 +193,7 @@ class BIM_PT_solar(bpy.types.Panel):
|
||||
row = self.layout.row()
|
||||
row.prop(props, "year")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(
|
||||
props,
|
||||
"month",
|
||||
text={
|
||||
1: "January",
|
||||
2: "February",
|
||||
3: "March",
|
||||
4: "April",
|
||||
5: "May",
|
||||
6: "June",
|
||||
7: "July",
|
||||
8: "August",
|
||||
9: "September",
|
||||
10: "October",
|
||||
11: "November",
|
||||
12: "December",
|
||||
}[props.month],
|
||||
)
|
||||
row.prop(props, "month", text=calendar.month_name[props.month])
|
||||
row.prop(props, "day")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -161,6 +161,10 @@ classes = (
|
||||
stair.FinishEditingStair,
|
||||
stair.EnableEditingStair,
|
||||
stair.RemoveStair,
|
||||
stair.ToggleStairTotalLengthLock,
|
||||
stair.AdjustStairTreads,
|
||||
stair.CycleStairType,
|
||||
stair.GizmoStairEdition,
|
||||
sverchok_modifier.CreateNewSverchokGraph,
|
||||
sverchok_modifier.UpdateDataFromSverchok,
|
||||
sverchok_modifier.DeleteSverchokGraph,
|
||||
@@ -172,12 +176,17 @@ classes = (
|
||||
window.FinishEditingWindow,
|
||||
window.EnableEditingWindow,
|
||||
window.RemoveWindow,
|
||||
window.CycleWindowType,
|
||||
window.GizmoWindowEdition,
|
||||
door.BIM_OT_add_door,
|
||||
door.AddDoor,
|
||||
door.CancelEditingDoor,
|
||||
door.FinishEditingDoor,
|
||||
door.EnableEditingDoor,
|
||||
door.RemoveDoor,
|
||||
door.ToggleDoorSwing,
|
||||
door.CycleDoorType,
|
||||
door.GizmoDoorEdition,
|
||||
railing.BIM_OT_add_railing,
|
||||
railing.CopyRailingParameters,
|
||||
railing.AddRailing,
|
||||
|
||||
@@ -33,13 +33,15 @@ import bonsai.core.geometry
|
||||
import bonsai.core.geometry as core
|
||||
import bonsai.core.root
|
||||
from bonsai.bim.module.model.window import create_bm_window, create_bm_box
|
||||
from bonsai.bim import gizmo
|
||||
from bonsai.bim.gizmo import GizmoPropConfig
|
||||
|
||||
from mathutils import Vector, Matrix
|
||||
from typing import Union, Any, Optional
|
||||
|
||||
import json
|
||||
import collections
|
||||
import collections.abc
|
||||
from typing import get_args
|
||||
|
||||
V_ = tool.Blender.V_
|
||||
|
||||
@@ -192,8 +194,8 @@ def bm_mirror(
|
||||
def create_bm_extruded_profile(
|
||||
bm: bmesh.types.BMesh,
|
||||
points: list[Vector],
|
||||
edges: Optional[list[tuple[int, int]]] = None,
|
||||
faces: Optional[list[list[int]]] = None,
|
||||
edges: list[tuple[int, int]] | None = None,
|
||||
faces: list[list[int]] | None = None,
|
||||
position: Vector = V_(0, 0, 0).freeze(),
|
||||
magnitude: float = 1.0,
|
||||
extrusion_vector: Vector = V_(0, 0, 1).freeze(),
|
||||
@@ -278,7 +280,7 @@ def create_bm_door_lining(
|
||||
new_faces = [bm.faces.new([new_verts[vi] for vi in face[1]]) for face in faces]
|
||||
|
||||
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
|
||||
extrusion_vector = Vector((0, 1, 0)) * depth
|
||||
extrusion_vector = V_(0, 1, 0) * depth
|
||||
translate_verts = [v for v in extruded["geom"] if isinstance(v, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
|
||||
|
||||
@@ -291,6 +293,8 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Model.get_door_props(obj)
|
||||
if not props.is_editing:
|
||||
return
|
||||
|
||||
overall_width = props.overall_width
|
||||
overall_height = props.overall_height
|
||||
@@ -408,7 +412,10 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
|
||||
if door_swing_type == "LEFT":
|
||||
bm_mirror(
|
||||
bm, door_handle_verts, mirror_axes=V_(1, 0, 0), mirror_point=panel_position + V_(panel_size.x / 2, 0, 0)
|
||||
bm,
|
||||
door_handle_verts,
|
||||
mirror_axes=V_(1, 0, 0),
|
||||
mirror_point=panel_position + V_(panel_size.x / 2, 0, 0),
|
||||
)
|
||||
|
||||
door_handle_mirrored_verts = bm_mirror(
|
||||
@@ -467,6 +474,7 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
lining_offset_verts = lining_verts + door_verts + window_lining_verts + frame_verts + glass_verts
|
||||
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=lining_offset_verts)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
|
||||
if bpy.context.active_object.mode == "EDIT":
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
@@ -478,14 +486,14 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
|
||||
class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_door"
|
||||
bl_label = "Door"
|
||||
bl_label = "Add Door"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return tool.Ifc.get() and context.mode == "OBJECT"
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
ifc_file = tool.Ifc.get()
|
||||
if not ifc_file:
|
||||
self.report({"ERROR"}, "You need to start IFC project first to create a door.")
|
||||
@@ -528,6 +536,8 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
assert element
|
||||
props = tool.Model.get_door_props(obj)
|
||||
|
||||
tool.Blender.get_addon_preferences().default_parameters.door.copy_to(props)
|
||||
|
||||
door_data = props.get_general_kwargs(convert_to_project_units=True)
|
||||
lining_props = props.get_lining_kwargs(convert_to_project_units=True)
|
||||
panel_props = props.get_panel_kwargs(convert_to_project_units=True)
|
||||
@@ -546,7 +556,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
update_door_modifier_representation(obj)
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
|
||||
continue
|
||||
@@ -557,6 +567,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_door"
|
||||
bl_label = "Cancel Editing Door on Selected Objects"
|
||||
bl_description = "Cancel editing and revert door parameters to their previous values"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def cancel_editing_door_on_object(self, obj: bpy.types.Object) -> None:
|
||||
@@ -591,6 +602,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_door"
|
||||
bl_label = "Finish Editing Door on Selected Objects"
|
||||
bl_description = "Apply changes and finish editing door parameters"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def finish_editing_door_on_object(self, obj):
|
||||
@@ -627,6 +639,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_door"
|
||||
bl_label = "Enable Editing Door on Selected Objects"
|
||||
bl_description = "Enter edit mode to modify door parameters interactively"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def edit_door_on_obj(self, obj):
|
||||
@@ -670,3 +683,290 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.remove_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"""Toggle door swing direction and optionally flip door geometry.
|
||||
|
||||
Shift+Click (when flip_geometry=True): Flip geometry only without changing door direction"""
|
||||
|
||||
bl_idname = "bim.toggle_door_swing"
|
||||
bl_label = "Toggle Door Swing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
flip_geometry: bpy.props.BoolProperty(name="Flip Geometry", default=False)
|
||||
flip_local_axes: bpy.props.EnumProperty(
|
||||
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
|
||||
)
|
||||
skip_direction_change: bpy.props.BoolProperty(
|
||||
name="Skip Direction Change", default=False, options={"HIDDEN", "SKIP_SAVE"}
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.skip_direction_change = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def _toggle_swing_direction(self, obj: bpy.types.Object) -> bool:
|
||||
"""Toggle door swing direction between LEFT and RIGHT."""
|
||||
props = tool.Model.get_door_props(obj)
|
||||
current_type = props.door_type
|
||||
|
||||
if "LEFT" in current_type:
|
||||
props.door_type = current_type.replace("LEFT", "RIGHT")
|
||||
return True
|
||||
elif "RIGHT" in current_type:
|
||||
props.door_type = current_type.replace("RIGHT", "LEFT")
|
||||
return True
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return {"CANCELLED"}
|
||||
|
||||
is_door = tool.Blender.Modifier.is_door(element)
|
||||
|
||||
if self.flip_geometry:
|
||||
tool.Geometry.flip_object(obj, self.flip_local_axes)
|
||||
if not self.skip_direction_change and is_door:
|
||||
self._toggle_swing_direction(obj)
|
||||
elif is_door:
|
||||
self._toggle_swing_direction(obj)
|
||||
else:
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CycleDoorType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"""Cycle through available door types. Shift+click to cycle in reverse."""
|
||||
|
||||
bl_idname = "bim.cycle_door_type"
|
||||
bl_label = "Cycle Door Type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.reverse = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Blender.Modifier.is_door(element):
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_door_props(obj)
|
||||
door_types = get_args(tool.Model.DoorType)
|
||||
current_index = door_types.index(props.door_type) if props.door_type in door_types else 0
|
||||
direction = -1 if self.reverse else 1
|
||||
next_index = (current_index + direction) % len(door_types)
|
||||
props.door_type = door_types[next_index]
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
bl_idname = "OBJECT_GGT_bim_door_edition"
|
||||
bl_label = "Door Editing Gizmo"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT"}
|
||||
|
||||
enable_editing_operator = "bim.enable_editing_door"
|
||||
finish_editing_operator = "bim.finish_editing_door"
|
||||
cancel_editing_operator = "bim.cancel_editing_door"
|
||||
cycle_type_operator = "bim.cycle_door_type"
|
||||
|
||||
gizmo_props = [
|
||||
GizmoPropConfig("overall_height", (0, 0, 1)),
|
||||
GizmoPropConfig("overall_width", (1, 0, 0)),
|
||||
GizmoPropConfig("threshold_thickness", (0, 0, 1)),
|
||||
GizmoPropConfig("threshold_depth", (0, 1, 0)),
|
||||
GizmoPropConfig("lining_depth", (0, 1, 0)),
|
||||
GizmoPropConfig("lining_thickness", (-1, 0, 0)),
|
||||
GizmoPropConfig("transom_offset", (0, 0, 1)),
|
||||
GizmoPropConfig("transom_thickness", (0, 0, 1)),
|
||||
GizmoPropConfig("casing_thickness", (-1, 0, 0)),
|
||||
GizmoPropConfig("casing_depth", (0, 1, 0)),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
return tool.Blender.Modifier.is_door(element)
|
||||
|
||||
def get_props(self, obj):
|
||||
return tool.Model.get_door_props(obj)
|
||||
|
||||
def get_gizmo_prefs(self):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return prefs.gizmos.door
|
||||
|
||||
def should_hide_gizmo(self, attr_name, props):
|
||||
"""Door-specific visibility rules for gizmos."""
|
||||
if not props.is_editing:
|
||||
return True
|
||||
if attr_name == "threshold_depth" and props.threshold_thickness == 0.0:
|
||||
return True
|
||||
if attr_name == "transom_offset" and props.transom_thickness == 0.0:
|
||||
return True
|
||||
if attr_name == "casing_thickness" and props.lining_offset != 0.0:
|
||||
return True
|
||||
if attr_name == "casing_depth" and (props.lining_offset != 0.0 or props.casing_thickness == 0.0):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_gizmo_matrix_overall_height(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_overall_width(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_threshold_thickness(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(
|
||||
props.overall_width / 2,
|
||||
props.threshold_offset + props.threshold_depth / 2 + props.lining_offset,
|
||||
props.threshold_thickness,
|
||||
)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_threshold_depth(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.threshold_depth + props.lining_offset, props.threshold_thickness / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_lining_depth(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_lining_thickness(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width - props.lining_thickness, props.lining_depth / 2, props.overall_height / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_transom_offset(self, props):
|
||||
# Position at the bottom of the transom (transom extends upward from offset)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.transom_offset)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_transom_thickness(self, props):
|
||||
# Position at the top of the transom (offset + thickness)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.transom_offset + props.transom_thickness)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
@staticmethod
|
||||
def _get_casing_gizmo_base_position(props) -> tuple[float, float, float]:
|
||||
"""Get common base position for casing gizmos."""
|
||||
x = -props.casing_thickness + props.lining_thickness
|
||||
y_base = props.lining_depth + props.lining_offset
|
||||
z = props.overall_height / 2
|
||||
return x, y_base, z
|
||||
|
||||
def get_gizmo_matrix_casing_thickness(self, props):
|
||||
x, y_base, z = self._get_casing_gizmo_base_position(props)
|
||||
translation = Matrix.Translation(V_(x, y_base + props.casing_depth / 2, z))
|
||||
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_casing_depth(self, props):
|
||||
x, y_base, z = self._get_casing_gizmo_base_position(props)
|
||||
translation = Matrix.Translation(V_(x, y_base + props.casing_depth, z))
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def setup(self, context):
|
||||
# Use base class methods for common gizmos
|
||||
self.setup_property_gizmos(context)
|
||||
self.setup_editing_gizmos(context)
|
||||
|
||||
# Door-specific: swing arc gizmos
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
highlight_color = prefs.decorator_color_selected[:3]
|
||||
inactive_color = prefs.decorator_color_background[:3]
|
||||
special_color = prefs.decorator_color_special[:3]
|
||||
|
||||
self.gizmo_door_type = self.gizmos.new("VIEW3D_GT_arc")
|
||||
self.gizmo_door_type.use_draw_scale = False
|
||||
self.gizmo_door_type.color = special_color
|
||||
self.gizmo_door_type.alpha = 0.5
|
||||
self.gizmo_door_type.color_highlight = highlight_color
|
||||
self.gizmo_door_type.prop_path = "BIMDoorProperties.door_type"
|
||||
op = self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
|
||||
op.flip_geometry = False
|
||||
|
||||
# Flip arc gizmo - mirrors the swing arc along X axis, flips door and toggles direction when clicked
|
||||
self.gizmo_flip_arc = self.gizmos.new("VIEW3D_GT_arc")
|
||||
self.gizmo_flip_arc.use_draw_scale = False
|
||||
self.gizmo_flip_arc.color = inactive_color
|
||||
self.gizmo_flip_arc.alpha = 0.5
|
||||
self.gizmo_flip_arc.color_highlight = highlight_color
|
||||
self.gizmo_flip_arc.prop_path = "BIMDoorProperties.door_type"
|
||||
op = self.gizmo_flip_arc.target_set_operator("bim.toggle_door_swing")
|
||||
op.flip_geometry = True
|
||||
op.flip_local_axes = "XY"
|
||||
|
||||
def refresh(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
props = self.get_props(obj)
|
||||
mw = obj.matrix_world
|
||||
self.update_property_gizmos(mw, props)
|
||||
self.update_swing_gizmos(mw, props)
|
||||
self.update_editing_gizmos(context, mw, props)
|
||||
|
||||
def update_swing_gizmos(self, mw, props):
|
||||
"""Update swing gizmo position and color based on editing state."""
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
door_gizmo_prefs = prefs.gizmos.door
|
||||
|
||||
self.gizmo_door_type.hide = not props.is_editing or not door_gizmo_prefs.swing_arc
|
||||
self.gizmo_flip_arc.hide = not props.is_editing or not door_gizmo_prefs.flip_arc
|
||||
|
||||
# If both are hidden, no need to calculate transforms
|
||||
if self.gizmo_door_type.hide and self.gizmo_flip_arc.hide:
|
||||
return
|
||||
|
||||
# Calculate base swing transformation (common to both arcs)
|
||||
swing_x_offset = props.overall_width if "RIGHT" in props.door_type else 0.0
|
||||
base_swing_transform = Matrix.Translation(V_(swing_x_offset, props.lining_offset, 0)) @ Matrix.Scale(
|
||||
props.overall_width, 4
|
||||
)
|
||||
|
||||
if not self.gizmo_door_type.hide:
|
||||
self.gizmo_door_type.matrix_basis = mw @ base_swing_transform
|
||||
self.gizmo_door_type.color = prefs.decorations_colour[:3]
|
||||
|
||||
if not self.gizmo_flip_arc.hide:
|
||||
# Mirror the flip arc along Y axis to show the opposite swing direction
|
||||
mirror_y = Matrix.Scale(-1, 4, (0, 1, 0))
|
||||
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_y
|
||||
|
||||
@@ -217,7 +217,12 @@ class BIMModelProperties(PropertyGroup):
|
||||
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
|
||||
x_angle: bpy.props.FloatProperty(
|
||||
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
|
||||
name="X Angle",
|
||||
default=0,
|
||||
subtype="ANGLE",
|
||||
min=math.radians(-180),
|
||||
max=math.radians(180),
|
||||
update=update_x_angle,
|
||||
)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
|
||||
@@ -348,24 +353,136 @@ class BIMArrayProperties(PropertyGroup):
|
||||
|
||||
|
||||
def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
"""Update tread_run when total_length_target changes"""
|
||||
# Calculate available length for default treads
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1 # number of risers
|
||||
|
||||
# Subtract custom first tread if not locked and not zero
|
||||
if not self.custom_tread_lock and self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
|
||||
# Subtract custom last tread if not locked and not zero
|
||||
if not self.custom_tread_lock and self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
# Calculate tread_run for remaining treads
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
# All treads are custom, just use target length
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
|
||||
|
||||
def update_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update either number_of_treads or total_length_target when tread_run changes"""
|
||||
if self.total_length_lock:
|
||||
self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1)
|
||||
# Calculate how much length custom treads take up
|
||||
custom_length = 0
|
||||
n_custom_treads = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[0]
|
||||
n_custom_treads += 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[1]
|
||||
n_custom_treads += 1
|
||||
|
||||
# Calculate how many default treads fit in remaining space
|
||||
available_length = self.total_length_target - custom_length
|
||||
if self.tread_run > 0:
|
||||
n_default_treads = available_length / self.tread_run
|
||||
total_treads = n_default_treads + n_custom_treads
|
||||
# number_of_treads = number_of_risers - 1
|
||||
self["number_of_treads"] = int(total_treads - 1)
|
||||
else:
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
# Calculate total length from current settings
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
|
||||
|
||||
def update_number_of_treads(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update either tread_run or total_length_target when number_of_treads changes"""
|
||||
if self.total_length_lock:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
# Calculate available length for default treads
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
else:
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
# Calculate total length from current settings
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
|
||||
|
||||
StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"]
|
||||
def update_custom_first_last_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update tread_run or total_length when custom treads change"""
|
||||
if self.total_length_lock:
|
||||
# Recalculate tread_run to maintain total length
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
# Recalculate total length
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
@@ -408,7 +525,7 @@ class BIMStairProperties(PropertyGroup):
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
|
||||
stair_type: bpy.props.EnumProperty(
|
||||
name="Stair Type",
|
||||
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(StairType)],
|
||||
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(tool.Model.StairType)],
|
||||
default="CONCRETE",
|
||||
update=validate_nosing_value,
|
||||
)
|
||||
@@ -425,6 +542,7 @@ class BIMStairProperties(PropertyGroup):
|
||||
min=0,
|
||||
unit="LENGTH",
|
||||
size=2,
|
||||
update=update_custom_first_last_tread_run, # Added update callback
|
||||
)
|
||||
nosing_length: bpy.props.FloatProperty(
|
||||
name="Nosing Length",
|
||||
@@ -492,16 +610,18 @@ class BIMStairProperties(PropertyGroup):
|
||||
}
|
||||
stair_kwargs.update(generic_props)
|
||||
|
||||
non_si_units_props = self.non_si_units_props
|
||||
# If locked, use tread_run for both first and last treads
|
||||
if self.custom_tread_lock:
|
||||
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||
non_si_units_props += ("custom_first_last_tread_run",)
|
||||
stair_kwargs["custom_first_last_tread_run"] = (None, None)
|
||||
else:
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
|
||||
if not convert_to_project_units:
|
||||
return stair_kwargs
|
||||
|
||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
|
||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, non_si_units_props)
|
||||
return stair_kwargs
|
||||
|
||||
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
|
||||
@@ -513,18 +633,32 @@ class BIMStairProperties(PropertyGroup):
|
||||
|
||||
def set_props_kwargs_from_ifc_data(self, kwargs):
|
||||
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
|
||||
tread_run = kwargs.get("tread_run", 0.3)
|
||||
|
||||
# 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)
|
||||
kwargs["custom_tread_lock"] = all(ct not in (0.0, tread_run) for ct in custom_treads)
|
||||
|
||||
if "custom_first_last_tread_run" in kwargs:
|
||||
custom_treads = kwargs["custom_first_last_tread_run"]
|
||||
custom_treads = [tread_run if v is None else v for v in custom_treads]
|
||||
kwargs["custom_first_last_tread_run"] = custom_treads
|
||||
|
||||
for prop_name in kwargs:
|
||||
setattr(self, prop_name, kwargs[prop_name])
|
||||
|
||||
def copy_to(self, target_props: "BIMStairProperties") -> None:
|
||||
"""Copy preset values to target stair properties."""
|
||||
target_props.custom_tread_lock = self.custom_tread_lock
|
||||
for prop_name, prop_value in self.get_props_kwargs().items():
|
||||
# Skip custom_first_last_tread_run if it contains None values (when lock is enabled)
|
||||
if prop_name == "custom_first_last_tread_run" and None in prop_value:
|
||||
continue
|
||||
setattr(target_props, prop_name, prop_value)
|
||||
|
||||
|
||||
class BIMSverchokProperties(PropertyGroup):
|
||||
node_group: bpy.props.PointerProperty(name="Node Group", type=NodeTree)
|
||||
@@ -537,19 +671,6 @@ def window_type_prop_update(self, context):
|
||||
update_window(self, context)
|
||||
|
||||
|
||||
WindowType = Literal[
|
||||
"SINGLE_PANEL",
|
||||
"DOUBLE_PANEL_HORIZONTAL",
|
||||
"DOUBLE_PANEL_VERTICAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
"TRIPLE_PANEL_LEFT",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_HORIZONTAL",
|
||||
"TRIPLE_PANEL_VERTICAL",
|
||||
]
|
||||
|
||||
|
||||
# default prop values are in mm and converted later
|
||||
class BIMWindowProperties(PropertyGroup):
|
||||
non_si_units_props = (
|
||||
@@ -578,7 +699,7 @@ class BIMWindowProperties(PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
window_type: bpy.props.EnumProperty(
|
||||
name="Window Type",
|
||||
items=[(i, i, "") for i in get_args(WindowType)],
|
||||
items=[(i, i, "") for i in get_args(tool.Model.WindowType)],
|
||||
default="SINGLE_PANEL",
|
||||
update=window_type_prop_update,
|
||||
)
|
||||
@@ -588,9 +709,11 @@ class BIMWindowProperties(PropertyGroup):
|
||||
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6, subtype="DISTANCE", update=update_window)
|
||||
|
||||
# lining properties
|
||||
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_window)
|
||||
lining_depth: bpy.props.FloatProperty(
|
||||
name="Lining Depth", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
|
||||
)
|
||||
lining_thickness: bpy.props.FloatProperty(
|
||||
name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_window
|
||||
name="Lining Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
|
||||
)
|
||||
lining_offset: bpy.props.FloatProperty(
|
||||
name="Lining Offset", default=0.050, subtype="DISTANCE", update=update_window
|
||||
@@ -619,12 +742,13 @@ class BIMWindowProperties(PropertyGroup):
|
||||
update=update_window,
|
||||
)
|
||||
transom_thickness: bpy.props.FloatProperty(
|
||||
name="Transom Thickness", default=0.050, subtype="DISTANCE", update=update_window
|
||||
name="Transom Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
|
||||
)
|
||||
first_transom_offset: bpy.props.FloatProperty(
|
||||
name="First Transom Offset",
|
||||
description="Distance from the first lining to the first transom center",
|
||||
default=0.3,
|
||||
min=0.001,
|
||||
subtype="DISTANCE",
|
||||
update=update_window,
|
||||
)
|
||||
@@ -651,7 +775,7 @@ class BIMWindowProperties(PropertyGroup):
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
window_type: WindowType
|
||||
window_type: tool.Model.WindowType
|
||||
overall_height: float
|
||||
overall_width: float
|
||||
lining_depth: float
|
||||
@@ -685,7 +809,7 @@ class BIMWindowProperties(PropertyGroup):
|
||||
return tool.Model.convert_data_to_project_units(kwargs, ["window_type"])
|
||||
|
||||
def get_lining_kwargs(
|
||||
self, window_type: Optional[WindowType] = None, convert_to_project_units: bool = False
|
||||
self, window_type: Optional[tool.Model.WindowType] = None, convert_to_project_units: bool = False
|
||||
) -> dict[str, Any]:
|
||||
if not window_type:
|
||||
window_type = self.window_type
|
||||
@@ -743,17 +867,13 @@ class BIMWindowProperties(PropertyGroup):
|
||||
for prop_name in kwargs:
|
||||
setattr(self, prop_name, kwargs[prop_name])
|
||||
|
||||
|
||||
DoorType = Literal[
|
||||
"SINGLE_SWING_LEFT",
|
||||
"SINGLE_SWING_RIGHT",
|
||||
"DOUBLE_SWING_LEFT",
|
||||
"DOUBLE_SWING_RIGHT",
|
||||
"DOUBLE_DOOR_SINGLE_SWING",
|
||||
"SLIDING_TO_LEFT",
|
||||
"SLIDING_TO_RIGHT",
|
||||
"DOUBLE_DOOR_SLIDING",
|
||||
]
|
||||
def copy_to(self, target_props: "BIMWindowProperties") -> None:
|
||||
"""Copy preset values to target window properties, excluding materials."""
|
||||
exclude_props = {"lining_material", "framing_material", "glazing_material"}
|
||||
for kwargs_dict in (self.get_general_kwargs(), self.get_lining_kwargs(), self.get_panel_kwargs()):
|
||||
for prop_name, prop_value in kwargs_dict.items():
|
||||
if prop_name not in exclude_props:
|
||||
setattr(target_props, prop_name, prop_value)
|
||||
|
||||
|
||||
class BIMDoorProperties(PropertyGroup):
|
||||
@@ -768,17 +888,39 @@ class BIMDoorProperties(PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
door_type: bpy.props.EnumProperty(
|
||||
name="Door Operation Type",
|
||||
items=tuple((i, i, "") for i in get_args(DoorType)),
|
||||
items=tuple((i, i, "") for i in get_args(tool.Model.DoorType)),
|
||||
default="SINGLE_SWING_LEFT",
|
||||
update=update_door,
|
||||
)
|
||||
overall_height: bpy.props.FloatProperty(name="Overall Height", default=2.0, subtype="DISTANCE", update=update_door)
|
||||
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.9, subtype="DISTANCE", update=update_door)
|
||||
overall_height: bpy.props.FloatProperty(
|
||||
name="Overall Height",
|
||||
default=2.0,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
overall_width: bpy.props.FloatProperty(
|
||||
name="Overall Width",
|
||||
default=0.9,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
|
||||
# lining properties
|
||||
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_door)
|
||||
lining_depth: bpy.props.FloatProperty(
|
||||
name="Lining Depth",
|
||||
default=0.050,
|
||||
min=0.001,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
lining_thickness: bpy.props.FloatProperty(
|
||||
name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_door
|
||||
name="Lining Thickness",
|
||||
default=0.050,
|
||||
min=0.001,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
lining_offset: bpy.props.FloatProperty(
|
||||
name="Lining Offset",
|
||||
@@ -800,6 +942,7 @@ class BIMDoorProperties(PropertyGroup):
|
||||
name="Transom Thickness",
|
||||
description="Set values > 0 to add a transom.\n" "`0.050 mm` is good as default value",
|
||||
default=0.000,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
@@ -807,6 +950,7 @@ class BIMDoorProperties(PropertyGroup):
|
||||
name="Transom Offset",
|
||||
description="Distance from the bottom door opening to the beginning of the transom (unlike windows)",
|
||||
default=1.525,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
@@ -815,20 +959,32 @@ class BIMDoorProperties(PropertyGroup):
|
||||
name="Casing Thickness",
|
||||
description="Set values > 0 and LiningOffset = 0 to add a casing.",
|
||||
default=0.075,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
casing_depth: bpy.props.FloatProperty(
|
||||
name="Casing Depth",
|
||||
default=0.005,
|
||||
min=0.001,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
casing_depth: bpy.props.FloatProperty(name="Casing Depth", default=0.005, subtype="DISTANCE", update=update_door)
|
||||
|
||||
threshold_thickness: bpy.props.FloatProperty(
|
||||
name="Threshold Thickness",
|
||||
description="Set values > 0 to add a threshold.",
|
||||
default=0.025,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
threshold_depth: bpy.props.FloatProperty(
|
||||
name="Threshold Depth", default=0.1, subtype="DISTANCE", update=update_door
|
||||
name="Threshold Depth",
|
||||
default=0.1,
|
||||
min=0.001,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
threshold_offset: bpy.props.FloatProperty(
|
||||
name="Threshold Offset",
|
||||
@@ -839,7 +995,13 @@ class BIMDoorProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
# panel properties
|
||||
panel_depth: bpy.props.FloatProperty(name="Panel Depth", default=0.035, subtype="DISTANCE", update=update_door)
|
||||
panel_depth: bpy.props.FloatProperty(
|
||||
name="Panel Depth",
|
||||
default=0.035,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
panel_width_ratio: bpy.props.FloatProperty(
|
||||
name="Panel Width Ratio",
|
||||
description="Width of this panel, given as ratio " "relative to the total clear opening width of the door",
|
||||
@@ -849,10 +1011,18 @@ class BIMDoorProperties(PropertyGroup):
|
||||
update=update_door,
|
||||
)
|
||||
frame_thickness: bpy.props.FloatProperty(
|
||||
name="Window Frame Thickness", default=0.035, subtype="DISTANCE", update=update_door
|
||||
name="Window Frame Thickness",
|
||||
default=0.035,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
frame_depth: bpy.props.FloatProperty(
|
||||
name="Window Frame Depth", default=0.035, subtype="DISTANCE", update=update_door
|
||||
name="Window Frame Depth",
|
||||
default=0.035,
|
||||
min=0,
|
||||
subtype="DISTANCE",
|
||||
update=update_door,
|
||||
)
|
||||
|
||||
# Material properties
|
||||
@@ -862,7 +1032,7 @@ class BIMDoorProperties(PropertyGroup):
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
door_type: DoorType
|
||||
door_type: tool.Model.DoorType
|
||||
overall_height: float
|
||||
overall_width: float
|
||||
|
||||
@@ -960,6 +1130,14 @@ class BIMDoorProperties(PropertyGroup):
|
||||
for prop_name in kwargs:
|
||||
setattr(self, prop_name, kwargs[prop_name])
|
||||
|
||||
def copy_to(self, target_props: "BIMDoorProperties") -> None:
|
||||
"""Copy properties to target door properties, excluding materials."""
|
||||
exclude_props = {"lining_material", "framing_material", "glazing_material"}
|
||||
for kwargs_dict in (self.get_general_kwargs(), self.get_lining_kwargs(), self.get_panel_kwargs()):
|
||||
for prop_name, prop_value in kwargs_dict.items():
|
||||
if prop_name not in exclude_props:
|
||||
setattr(target_props, prop_name, prop_value)
|
||||
|
||||
|
||||
RailingType = Literal["FRAMELESS_PANEL", "WALL_MOUNTED_HANDRAIL"]
|
||||
CapType = Literal["TO_END_POST_AND_FLOOR", "TO_END_POST", "TO_FLOOR", "TO_WALL", "180", "NONE"]
|
||||
@@ -1059,6 +1237,11 @@ class BIMRailingProperties(PropertyGroup):
|
||||
for prop_name in kwargs:
|
||||
setattr(self, prop_name, kwargs[prop_name])
|
||||
|
||||
def copy_to(self, target_props: "BIMRailingProperties") -> None:
|
||||
"""Copy preset values to target railing properties."""
|
||||
for prop_name, prop_value in self.get_general_kwargs().items():
|
||||
setattr(target_props, prop_name, prop_value)
|
||||
|
||||
|
||||
def to_angle(percentage: float) -> float:
|
||||
return math.atan(percentage / 100)
|
||||
@@ -1161,6 +1344,11 @@ class BIMRoofProperties(PropertyGroup):
|
||||
for prop_name in kwargs:
|
||||
setattr(self, prop_name, kwargs[prop_name])
|
||||
|
||||
def copy_to(self, target_props: "BIMRoofProperties") -> None:
|
||||
"""Copy preset values to target roof properties."""
|
||||
for prop_name, prop_value in self.get_general_kwargs().items():
|
||||
setattr(target_props, prop_name, prop_value)
|
||||
|
||||
|
||||
class SnapMousePoint(PropertyGroup):
|
||||
x: bpy.props.FloatProperty(name="X")
|
||||
|
||||
@@ -106,6 +106,13 @@ def update_railing_modifier_ifc_data(context: bpy.types.Context) -> None:
|
||||
model_representation = ifcopenshell.api.geometry.add_railing_representation(ifc_file, **representation_data)
|
||||
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
|
||||
|
||||
# recalculate normals to ensure correct shading
|
||||
mesh = obj.data
|
||||
if isinstance(mesh, bpy.types.Mesh):
|
||||
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=False)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
|
||||
tool.Blender.apply_bmesh(mesh, bm)
|
||||
|
||||
elif props.railing_type == "FRAMELESS_PANEL":
|
||||
tool.Model.add_body_representation(obj)
|
||||
|
||||
@@ -335,6 +342,8 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Model.get_railing_props(obj)
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
tool.Blender.get_addon_preferences().default_parameters.railing.copy_to(props)
|
||||
|
||||
railing_data = props.get_general_kwargs(convert_to_project_units=True)
|
||||
path_data = get_path_data(obj)
|
||||
|
||||
|
||||
@@ -570,6 +570,8 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Model.get_roof_props(obj)
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
tool.Blender.get_addon_preferences().default_parameters.roof.copy_to(props)
|
||||
|
||||
# rejecting original roof shape to be safe
|
||||
# taking into account only it's bounding box dimensions
|
||||
if obj.dimensions.x == 0 or obj.dimensions.y == 0:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import json
|
||||
import bmesh
|
||||
import math
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
@@ -26,11 +27,14 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.core.root
|
||||
import bonsai.tool as tool
|
||||
from mathutils import Vector
|
||||
from bonsai.bim import gizmo
|
||||
from bonsai.bim.gizmo import GizmoPropConfig
|
||||
from mathutils import Vector, Matrix
|
||||
from bmesh.types import BMVert
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
from typing import get_args
|
||||
|
||||
|
||||
def regenerate_stair_mesh(obj: bpy.types.Object) -> None:
|
||||
@@ -131,7 +135,7 @@ def update_ifc_stair_props(obj: bpy.types.Object) -> None:
|
||||
|
||||
class BIM_OT_add_stair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_stair"
|
||||
bl_label = "Stair"
|
||||
bl_label = "Add Stair"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
@@ -188,6 +192,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
tool.Blender.get_addon_preferences().default_parameters.stair.copy_to(props)
|
||||
|
||||
# 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")
|
||||
@@ -291,3 +297,388 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleStairTotalLengthLock(bpy.types.Operator):
|
||||
"""Toggle the total length lock for stair editing"""
|
||||
|
||||
bl_idname = "bim.toggle_stair_total_length_lock"
|
||||
bl_label = "Toggle Stair Total Length Lock"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
props.total_length_lock = not props.total_length_lock
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AdjustStairTreads(bpy.types.Operator):
|
||||
"""Adjust the number of treads"""
|
||||
|
||||
bl_idname = "bim.adjust_stair_treads"
|
||||
bl_label = "Adjust Stair Treads"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
increment: IntProperty(name="Increment", default=1)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
new_value = props.number_of_treads + self.increment
|
||||
if new_value >= 1:
|
||||
props.number_of_treads = new_value
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CycleStairType(bpy.types.Operator):
|
||||
"""Cycle through stair types. Shift+click to cycle in reverse."""
|
||||
|
||||
bl_idname = "bim.cycle_stair_type"
|
||||
bl_label = "Cycle Stair Type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.reverse = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
stair_types = get_args(tool.Model.StairType)
|
||||
current_idx = stair_types.index(props.stair_type) if props.stair_type in stair_types else 0
|
||||
direction = -1 if self.reverse else 1
|
||||
props.stair_type = stair_types[(current_idx + direction) % len(stair_types)]
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
bl_idname = "OBJECT_GGT_bim_stair_edition"
|
||||
bl_label = "Stair Editing Gizmo"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT"}
|
||||
|
||||
# Gizmo layout offsets (meters)
|
||||
GIZMO_X_OFFSET = 0.5 # Horizontal offset from stair end
|
||||
GIZMO_PLUS_X_OFFSET = 0.25 # Additional X offset for plus button
|
||||
GIZMO_MINUS_X_OFFSET = 0.5 # Additional X offset for minus button
|
||||
GIZMO_BUTTON_Z_OFFSET = 0.15 # Vertical offset for +/- buttons above lock
|
||||
GIZMO_CYCLE_Z_OFFSET = 0.5 # Vertical offset for cycle gizmo above lock
|
||||
|
||||
enable_editing_operator = "bim.enable_editing_stair"
|
||||
finish_editing_operator = "bim.finish_editing_stair"
|
||||
cancel_editing_operator = "bim.cancel_editing_stair"
|
||||
|
||||
gizmo_props = [
|
||||
GizmoPropConfig("width", (0, 1, 0)),
|
||||
GizmoPropConfig("height", (0, 0, 1)),
|
||||
GizmoPropConfig("tread_run", (1, 0, 0)),
|
||||
GizmoPropConfig("tread_depth", (0, 0, -1)),
|
||||
GizmoPropConfig("nosing_length", (-1, 0, 0)),
|
||||
GizmoPropConfig("nosing_depth", (0, 0, -1)),
|
||||
GizmoPropConfig("total_length_target", (1, 0, 0)),
|
||||
GizmoPropConfig("base_slab_depth", (0, 0, -1)),
|
||||
GizmoPropConfig("top_slab_depth", (0, 0, -1)),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
return tool.Blender.Modifier.is_stair(element)
|
||||
|
||||
def get_props(self, obj):
|
||||
return tool.Model.get_stair_props(obj)
|
||||
|
||||
def get_gizmo_prefs(self):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return prefs.gizmos.stair
|
||||
|
||||
def should_hide_gizmo(self, attr_name, props):
|
||||
"""Stair-specific visibility rules for gizmos."""
|
||||
if not props.is_editing:
|
||||
return True
|
||||
# Hide concrete-specific gizmos for non-concrete stairs
|
||||
if attr_name in ("base_slab_depth", "top_slab_depth") and props.stair_type != "CONCRETE":
|
||||
return True
|
||||
# Hide tread_depth for generic stairs (has no tread geometry)
|
||||
if attr_name == "tread_depth" and props.stair_type == "GENERIC":
|
||||
return True
|
||||
# Hide nosing_depth when nosing_length is 0 or for Wood/Steel stair types
|
||||
if attr_name == "nosing_depth" and (props.nosing_length == 0.0 or props.stair_type == "WOOD/STEEL"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_stair_total_run(props) -> float:
|
||||
"""Calculate the total horizontal run of the stair."""
|
||||
return props.tread_run * props.number_of_treads
|
||||
|
||||
@staticmethod
|
||||
def _get_first_riser_height(props) -> float:
|
||||
"""Calculate the height of the first riser."""
|
||||
return props.height / (props.number_of_treads + 1)
|
||||
|
||||
def get_gizmo_matrix_width(self, props):
|
||||
translation = Matrix.Translation(Vector((0, props.width, 0)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_width_top(self, props):
|
||||
"""Position for the secondary width gizmo at the top of the stairs."""
|
||||
total_run = self._get_stair_total_run(props)
|
||||
translation = Matrix.Translation(Vector((total_run, props.width, props.height)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_height(self, props):
|
||||
total_run = self._get_stair_total_run(props)
|
||||
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_tread_run(self, props):
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
translation = Matrix.Translation(Vector((props.tread_run, props.width / 2, riser_height)))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_tread_depth(self, props):
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
translation = Matrix.Translation(
|
||||
Vector((props.tread_run / 2, props.width / 2, riser_height - props.tread_depth))
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, -1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_nosing_length(self, props):
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
translation = Matrix.Translation(Vector((-props.nosing_length, props.width / 2, riser_height)))
|
||||
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_nosing_depth(self, props):
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
translation = Matrix.Translation(
|
||||
Vector((-props.nosing_length, props.width / 2, riser_height - props.nosing_depth))
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, -1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_total_length_target(self, props):
|
||||
translation = Matrix.Translation(Vector((props.total_length_target, props.width / 2, props.height)))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_base_slab_depth(self, props):
|
||||
translation = Matrix.Translation(Vector((0, props.width / 2, -props.base_slab_depth)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, -1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_top_slab_depth(self, props):
|
||||
total_run = self._get_stair_total_run(props)
|
||||
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height - props.top_slab_depth)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, -1))
|
||||
return translation @ rotation
|
||||
|
||||
def setup(self, context):
|
||||
self.setup_property_gizmos(context)
|
||||
self.setup_editing_gizmos(context)
|
||||
|
||||
# Stair-specific gizmos
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
highlight_color = prefs.decorator_color_selected[:3]
|
||||
|
||||
# Secondary width gizmo at the top of the stairs (linked to same width property)
|
||||
self.gizmo_width_top = self.gizmos.new("BIM_GT_gizmo_arrow_2d")
|
||||
|
||||
def make_width_get():
|
||||
def move_get():
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return 0.0
|
||||
props = self.get_props(obj)
|
||||
return props.width
|
||||
return move_get
|
||||
|
||||
def make_width_set():
|
||||
def move_set(value):
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
props = self.get_props(obj)
|
||||
props.width = max(0.0, value)
|
||||
return move_set
|
||||
|
||||
self.gizmo_width_top.move_get_cb = make_width_get()
|
||||
self.gizmo_width_top.move_set_cb = make_width_set()
|
||||
self.gizmo_width_top.axis = Vector((0, 1, 0))
|
||||
self.gizmo_width_top.local_axis = Vector((0, 1, 0))
|
||||
self.gizmo_width_top.invert_delta = False
|
||||
self.gizmo_width_top.delta_scale = 1.0
|
||||
self.gizmo_width_top.prop_name = "Width"
|
||||
self.gizmo_width_top.gizmo_group = self
|
||||
self.gizmo_width_top.color = self.COLOR_GREEN
|
||||
self.gizmo_width_top.color_highlight = highlight_color
|
||||
self.gizmo_width_top.alpha = 0.99
|
||||
self.gizmo_width_top.use_draw_modal = True
|
||||
self.gizmo_width_top.use_draw_scale = True
|
||||
|
||||
# Total length lock gizmo
|
||||
self.lock_gizmo = self.gizmos.new("VIEW3D_GT_lock")
|
||||
self.lock_gizmo.use_draw_scale = False
|
||||
self.lock_gizmo.color = self.COLOR_BLUE
|
||||
self.lock_gizmo.color_highlight = highlight_color
|
||||
self.lock_gizmo.alpha = 0.8
|
||||
self.lock_gizmo.prop_path = "BIMStairProperties.total_length_lock"
|
||||
self.lock_gizmo.target_set_operator("bim.toggle_stair_total_length_lock")
|
||||
|
||||
# Plus gizmo for increasing treads
|
||||
self.plus_gizmo = self.gizmos.new("VIEW3D_GT_plus")
|
||||
self.plus_gizmo.use_draw_scale = False
|
||||
self.plus_gizmo.color = self.COLOR_GREEN
|
||||
self.plus_gizmo.color_highlight = highlight_color
|
||||
self.plus_gizmo.alpha = 0.8
|
||||
op = self.plus_gizmo.target_set_operator("bim.adjust_stair_treads")
|
||||
op.increment = 1
|
||||
|
||||
# Minus gizmo for decreasing treads
|
||||
self.minus_gizmo = self.gizmos.new("VIEW3D_GT_minus")
|
||||
self.minus_gizmo.use_draw_scale = False
|
||||
self.minus_gizmo.color = self.COLOR_RED
|
||||
self.minus_gizmo.color_highlight = highlight_color
|
||||
self.minus_gizmo.alpha = 0.8
|
||||
op = self.minus_gizmo.target_set_operator("bim.adjust_stair_treads")
|
||||
op.increment = -1
|
||||
|
||||
# Cycle gizmo for stair type
|
||||
default_color = prefs.decorations_colour[:3]
|
||||
self.cycle_gizmo = self.gizmos.new("VIEW3D_GT_cycle")
|
||||
self.cycle_gizmo.use_draw_scale = False
|
||||
self.cycle_gizmo.color = default_color
|
||||
self.cycle_gizmo.color_highlight = highlight_color
|
||||
self.cycle_gizmo.alpha = 0.8
|
||||
self.cycle_gizmo.target_set_operator("bim.cycle_stair_type")
|
||||
|
||||
def refresh(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
props = self.get_props(obj)
|
||||
mw = obj.matrix_world
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
self.update_property_gizmos(mw, props)
|
||||
self.update_editing_gizmos(context, mw, props)
|
||||
self.update_width_top_gizmo(mw, props)
|
||||
self.update_lock_gizmo(mw, props, billboard_rot)
|
||||
self.update_tread_count_gizmos(mw, props, billboard_rot)
|
||||
self.update_cycle_gizmo(mw, props, billboard_rot)
|
||||
|
||||
def update_width_top_gizmo(self, mw, props):
|
||||
"""Update the secondary width gizmo at the top of the stairs."""
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
# Use same visibility as the main width gizmo
|
||||
self.gizmo_width_top.hide = not props.is_editing or not getattr(gizmo_prefs, "width", True)
|
||||
|
||||
if self.gizmo_width_top.hide:
|
||||
return
|
||||
|
||||
self.gizmo_width_top.matrix_basis = mw @ self.get_gizmo_matrix_width_top(props)
|
||||
self.gizmo_width_top.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
|
||||
|
||||
def update_lock_gizmo(self, mw, props, billboard_rot):
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.lock_gizmo.hide = not props.is_editing or not gizmo_prefs.lock
|
||||
|
||||
if self.lock_gizmo.hide:
|
||||
return
|
||||
|
||||
self.lock_gizmo.color = self.COLOR_RED if props.total_length_lock else self.COLOR_GREEN
|
||||
|
||||
total_run = self._get_stair_total_run(props)
|
||||
lock_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
|
||||
local_transform = (
|
||||
Matrix.Translation(Vector((lock_x, props.width / 2, props.height)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(0.2, 4)
|
||||
)
|
||||
self.lock_gizmo.matrix_basis = mw @ local_transform
|
||||
|
||||
def update_tread_count_gizmos(self, mw, props, billboard_rot):
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
|
||||
self.plus_gizmo.hide = not props.is_editing or not gizmo_prefs.plus
|
||||
self.minus_gizmo.hide = not props.is_editing or props.number_of_treads <= 1 or not gizmo_prefs.minus
|
||||
|
||||
if self.plus_gizmo.hide and self.minus_gizmo.hide:
|
||||
return
|
||||
|
||||
total_run = self._get_stair_total_run(props)
|
||||
base_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
|
||||
|
||||
if not self.plus_gizmo.hide:
|
||||
plus_local_transform = (
|
||||
Matrix.Translation(Vector((
|
||||
base_x + self.GIZMO_PLUS_X_OFFSET,
|
||||
props.width / 2,
|
||||
props.height + self.GIZMO_BUTTON_Z_OFFSET
|
||||
)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(0.2, 4)
|
||||
)
|
||||
self.plus_gizmo.matrix_basis = mw @ plus_local_transform
|
||||
|
||||
if not self.minus_gizmo.hide:
|
||||
minus_local_transform = (
|
||||
Matrix.Translation(Vector((
|
||||
base_x + self.GIZMO_MINUS_X_OFFSET,
|
||||
props.width / 2,
|
||||
props.height + self.GIZMO_BUTTON_Z_OFFSET
|
||||
)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(0.2, 4)
|
||||
)
|
||||
self.minus_gizmo.matrix_basis = mw @ minus_local_transform
|
||||
|
||||
def update_cycle_gizmo(self, mw, props, billboard_rot):
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.cycle_gizmo.hide = not props.is_editing or not gizmo_prefs.cycle
|
||||
|
||||
if self.cycle_gizmo.hide:
|
||||
return
|
||||
|
||||
total_run = self._get_stair_total_run(props)
|
||||
cycle_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
|
||||
local_transform = (
|
||||
Matrix.Translation(Vector((
|
||||
cycle_x,
|
||||
props.width / 2,
|
||||
props.height + self.GIZMO_CYCLE_Z_OFFSET
|
||||
)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(0.2, 4)
|
||||
)
|
||||
self.cycle_gizmo.matrix_basis = mw @ local_transform
|
||||
|
||||
def draw_prepare(self, context):
|
||||
"""Called before drawing - updates gizmos to face camera."""
|
||||
# Call base class implementation
|
||||
super().draw_prepare(context)
|
||||
|
||||
# Also update the secondary width gizmo to face camera
|
||||
if hasattr(self, "gizmo_width_top") and not self.gizmo_width_top.hide:
|
||||
self.gizmo_width_top.draw_prepare(context)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bl_ui_utils.layout
|
||||
import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, Menu
|
||||
@@ -36,7 +35,12 @@ from bonsai.bim.module.model.stair import regenerate_stair_mesh
|
||||
from bonsai.bim.module.model.railing import update_railing_modifier_bmesh
|
||||
from bonsai.bim.module.model.roof import update_roof_modifier_bmesh
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING or bpy.app.version >= (5, 0, 0):
|
||||
import _bl_ui_utils.layout as bl_ui_utils_layout
|
||||
else:
|
||||
import bl_ui_utils.layout as bl_ui_utils_layout
|
||||
|
||||
|
||||
class BIM_MT_type_manager_menu(bpy.types.Menu):
|
||||
@@ -56,7 +60,7 @@ class BIM_MT_type_menu(bpy.types.Menu):
|
||||
def draw(self, context):
|
||||
props = tool.Model.get_model_props()
|
||||
layout = self.layout
|
||||
with bl_ui_utils.layout.operator_context(layout, "INVOKE_REGION_WIN"):
|
||||
with bl_ui_utils_layout.operator_context(layout, "INVOKE_REGION_WIN"):
|
||||
op = layout.operator("bim.rename_type", icon="GREASEPENCIL", text="Rename Type")
|
||||
op.element = props.menu_relating_type_id
|
||||
op = layout.operator("bim.select_type", icon="OBJECT_DATA")
|
||||
@@ -301,48 +305,9 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
|
||||
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)
|
||||
draw_stair_properties(self.layout, props)
|
||||
|
||||
# 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")
|
||||
row_length.prop(
|
||||
props,
|
||||
"total_length_lock",
|
||||
text="",
|
||||
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
|
||||
)
|
||||
regenerate_stair_mesh(obj)
|
||||
else:
|
||||
calculated_params = StairData.data["calculated_params"]
|
||||
@@ -436,39 +401,11 @@ class BIM_PT_window(bpy.types.Panel):
|
||||
row.label(text="Window parameters", icon="OUTLINER_OB_LATTICE")
|
||||
|
||||
if props.is_editing:
|
||||
number_of_panels, panels_data = props.window_types_panels[props.window_type]
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
for prop in general_props:
|
||||
self.layout.prop(props, prop)
|
||||
|
||||
lining_props = props.get_lining_kwargs()
|
||||
self.layout.label(text="Lining properties")
|
||||
for prop in lining_props:
|
||||
self.layout.prop(props, prop)
|
||||
|
||||
panel_props = props.get_panel_kwargs()
|
||||
self.layout.label(text="Panel properties")
|
||||
|
||||
panel_box = self.layout.box()
|
||||
row = panel_box.row()
|
||||
cols = [row.column(align=True) for i in range(number_of_panels + 1)]
|
||||
|
||||
cols[0].label(text="")
|
||||
|
||||
for panel_i in range(number_of_panels):
|
||||
r = cols[panel_i + 1].row()
|
||||
r.alignment = "CENTER"
|
||||
r.label(text=f"#{panel_i}")
|
||||
r = cols[panel_i + 1].row()
|
||||
|
||||
for prop in panel_props:
|
||||
cols[0].label(text=f"{props.bl_rna.properties[prop].name}")
|
||||
for panel_i in range(number_of_panels):
|
||||
cols[panel_i + 1].prop(props, prop, index=panel_i, text="")
|
||||
draw_window_properties(self.layout, props)
|
||||
|
||||
self.layout.use_property_split = True
|
||||
self.layout.label(text="Material Properties")
|
||||
@@ -556,19 +493,7 @@ class BIM_PT_door(bpy.types.Panel):
|
||||
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
for prop in general_props:
|
||||
self.layout.prop(props, prop)
|
||||
|
||||
lining_props = props.get_lining_kwargs()
|
||||
self.layout.label(text="Lining Properties")
|
||||
for prop in lining_props:
|
||||
self.layout.prop(props, prop)
|
||||
|
||||
panel_props = props.get_panel_kwargs()
|
||||
self.layout.label(text="Panel Properties")
|
||||
for prop in panel_props:
|
||||
self.layout.prop(props, prop)
|
||||
draw_door_properties(self.layout, props)
|
||||
|
||||
self.layout.use_property_split = True
|
||||
self.layout.label(text="Material Properties")
|
||||
@@ -639,14 +564,7 @@ class BIM_PT_railing(bpy.types.Panel):
|
||||
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
for prop in general_props:
|
||||
if prop == "support_spacing" and props.use_manual_supports:
|
||||
row = self.layout.row()
|
||||
row.prop(props, prop)
|
||||
row.active = False
|
||||
continue
|
||||
self.layout.prop(props, prop)
|
||||
draw_railing_properties(self.layout, props)
|
||||
|
||||
update_railing_modifier_bmesh(context)
|
||||
|
||||
@@ -704,9 +622,7 @@ class BIM_PT_roof(bpy.types.Panel):
|
||||
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
for prop in general_props:
|
||||
self.layout.prop(props, prop)
|
||||
draw_roof_properties(self.layout, props)
|
||||
|
||||
update_roof_modifier_bmesh(obj)
|
||||
elif props.is_editing_path:
|
||||
@@ -772,6 +688,126 @@ class BIM_PT_external_parametric_geometry(bpy.types.Panel):
|
||||
row.prop(input, "default_value", text=input.name)
|
||||
|
||||
|
||||
def draw_door_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
|
||||
"""Draw door properties UI (shared between properties panel and preferences)."""
|
||||
# General properties
|
||||
general_props = props.get_general_kwargs()
|
||||
for prop in general_props:
|
||||
layout.prop(props, prop)
|
||||
|
||||
# Lining properties
|
||||
layout.label(text="Lining Properties")
|
||||
lining_props = props.get_lining_kwargs()
|
||||
for prop in lining_props:
|
||||
layout.prop(props, prop)
|
||||
|
||||
# Panel properties
|
||||
layout.label(text="Panel Properties")
|
||||
panel_props = props.get_panel_kwargs()
|
||||
for prop in panel_props:
|
||||
layout.prop(props, prop)
|
||||
|
||||
|
||||
def draw_window_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
|
||||
"""Draw window properties UI (shared between properties panel and preferences)."""
|
||||
number_of_panels, panels_data = props.window_types_panels[props.window_type]
|
||||
|
||||
# General and lining properties
|
||||
general_props = props.get_general_kwargs()
|
||||
for prop in general_props:
|
||||
layout.prop(props, prop)
|
||||
|
||||
layout.label(text="Lining Properties")
|
||||
lining_props = props.get_lining_kwargs()
|
||||
for prop in lining_props:
|
||||
layout.prop(props, prop)
|
||||
|
||||
# Panel properties (special layout for multiple panels)
|
||||
panel_props = props.get_panel_kwargs()
|
||||
layout.label(text="Panel Properties")
|
||||
|
||||
panel_box = layout.box()
|
||||
row = panel_box.row()
|
||||
cols = [row.column(align=True) for i in range(number_of_panels + 1)]
|
||||
|
||||
cols[0].label(text="")
|
||||
|
||||
for panel_i in range(number_of_panels):
|
||||
r = cols[panel_i + 1].row()
|
||||
r.alignment = "CENTER"
|
||||
r.label(text=f"#{panel_i}")
|
||||
r = cols[panel_i + 1].row()
|
||||
|
||||
for prop in panel_props:
|
||||
cols[0].label(text=f"{props.bl_rna.properties[prop].name}")
|
||||
for panel_i in range(number_of_panels):
|
||||
cols[panel_i + 1].prop(props, prop, index=panel_i, text="")
|
||||
|
||||
|
||||
def draw_railing_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
|
||||
"""Draw railing properties UI (shared between properties panel and preferences)."""
|
||||
general_props = props.get_general_kwargs()
|
||||
for prop in general_props:
|
||||
if prop == "support_spacing" and props.use_manual_supports:
|
||||
row = layout.row()
|
||||
row.prop(props, prop)
|
||||
row.active = False
|
||||
continue
|
||||
layout.prop(props, prop)
|
||||
|
||||
|
||||
def draw_roof_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
|
||||
"""Draw roof properties UI (shared between properties panel and preferences)."""
|
||||
# General properties
|
||||
general_props = props.get_general_kwargs()
|
||||
for prop in general_props:
|
||||
layout.prop(props, prop)
|
||||
|
||||
|
||||
def draw_stair_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
|
||||
"""Draw stair properties UI (shared between properties panel and preferences)."""
|
||||
for prop_name in props.get_props_kwargs():
|
||||
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
|
||||
if prop_name == "custom_tread_lock":
|
||||
continue
|
||||
|
||||
prop_value = getattr(props, prop_name)
|
||||
|
||||
# Special handling for custom_first_last_tread_run
|
||||
if prop_name == "custom_first_last_tread_run":
|
||||
# Draw the lock toggle
|
||||
row_lock = 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
|
||||
layout.label(text=f"{prop_readable_name}:")
|
||||
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
|
||||
layout.label(text=f"{prop_readable_name}:")
|
||||
layout.prop(props, prop_name, text="")
|
||||
else:
|
||||
layout.prop(props, prop_name)
|
||||
|
||||
if prop_name == "height": # Weak but we just want to insert this inside props drawing
|
||||
row_length = layout.row(align=True)
|
||||
row_length.prop(props, "total_length_target")
|
||||
row_length.prop(
|
||||
props,
|
||||
"total_length_lock",
|
||||
text="",
|
||||
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
|
||||
)
|
||||
|
||||
|
||||
def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
|
||||
self.layout.operator_context = "INVOKE_REGION_WIN"
|
||||
self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element")
|
||||
|
||||
@@ -28,6 +28,8 @@ import ifcopenshell.api.pset
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.root
|
||||
import bonsai.core.geometry
|
||||
from bonsai.bim import gizmo
|
||||
from bonsai.bim.gizmo import GizmoPropConfig
|
||||
from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.material
|
||||
@@ -36,8 +38,8 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.unit
|
||||
from bmesh.types import BMVert
|
||||
from mathutils import Vector
|
||||
from typing import Optional, Union
|
||||
from mathutils import Vector, Matrix
|
||||
from typing import get_args
|
||||
|
||||
V_ = tool.Blender.V_
|
||||
|
||||
@@ -139,7 +141,7 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None:
|
||||
|
||||
|
||||
def create_bm_window_frame(
|
||||
bm: bmesh.types.BMesh, size: Vector, thickness: Union[float, list[float]], position: Vector = V_(0, 0, 0).freeze()
|
||||
bm: bmesh.types.BMesh, size: Vector, thickness: float | list[float], position: Vector = V_(0, 0, 0).freeze()
|
||||
) -> list[bmesh.types.BMVert]:
|
||||
"""`thickness` of the profile is defined as list in the following order:
|
||||
`(LEFT, TOP, RIGHT, BOTTOM)`
|
||||
@@ -226,7 +228,7 @@ def create_bm_window(
|
||||
frame_thickness: float,
|
||||
glass_thickness: float,
|
||||
position: Vector,
|
||||
x_offsets: Optional[list] = None,
|
||||
x_offsets: list | None = None,
|
||||
) -> tuple[list[bmesh.types.BMVert], list[bmesh.types.BMVert], list[bmesh.types.BMVert]]:
|
||||
"""`lining_thickness` and `x_offsets` are expected to be defined as a list,
|
||||
similarly to `create_bm_window_frame` `thickness` argument"""
|
||||
@@ -258,6 +260,9 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Model.get_window_props(obj)
|
||||
if not props.is_editing:
|
||||
return
|
||||
|
||||
panel_schema = DEFAULT_PANEL_SCHEMAS[props.window_type]
|
||||
accumulated_height = [0] * len(panel_schema[0])
|
||||
built_panels = []
|
||||
@@ -388,6 +393,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
|
||||
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=bm.verts)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
|
||||
if bpy.context.active_object.mode == "EDIT":
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
@@ -399,7 +405,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
|
||||
class BIM_OT_add_window(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_window"
|
||||
bl_label = "Window"
|
||||
bl_label = "Add Window"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
@@ -451,6 +457,8 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
assert element
|
||||
props = tool.Model.get_window_props(obj)
|
||||
|
||||
tool.Blender.get_addon_preferences().default_parameters.window.copy_to(props)
|
||||
|
||||
window_data = props.get_general_kwargs(convert_to_project_units=True)
|
||||
lining_props = props.get_lining_kwargs(convert_to_project_units=True)
|
||||
panel_props = props.get_panel_kwargs(convert_to_project_units=True)
|
||||
@@ -570,3 +578,229 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CycleWindowType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"""Cycle through available window types."""
|
||||
|
||||
bl_idname = "bim.cycle_window_type"
|
||||
bl_label = "Cycle Window Type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Blender.Modifier.is_window(element):
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_window_props(obj)
|
||||
window_types = list(get_args(tool.Model.WindowType))
|
||||
current_index = window_types.index(props.window_type) if props.window_type in window_types else 0
|
||||
next_index = (current_index + 1) % len(window_types)
|
||||
props.window_type = window_types[next_index]
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
bl_idname = "OBJECT_GGT_bim_window_edition"
|
||||
bl_label = "Window Editing Gizmo"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT"}
|
||||
|
||||
enable_editing_operator = "bim.enable_editing_window"
|
||||
finish_editing_operator = "bim.finish_editing_window"
|
||||
cancel_editing_operator = "bim.cancel_editing_window"
|
||||
cycle_type_operator = "bim.cycle_window_type"
|
||||
|
||||
gizmo_props = [
|
||||
GizmoPropConfig("overall_height", (0, 0, 1)),
|
||||
GizmoPropConfig("overall_width", (1, 0, 0)),
|
||||
GizmoPropConfig("lining_depth", (0, 1, 0)),
|
||||
GizmoPropConfig("lining_thickness", (1, 0, 0)),
|
||||
GizmoPropConfig("lining_to_panel_offset_x", (1, 0, 0)),
|
||||
GizmoPropConfig("lining_to_panel_offset_y", (0, 1, 0)),
|
||||
GizmoPropConfig("mullion_thickness", (1, 0, 0), delta_scale=2.0),
|
||||
GizmoPropConfig("first_mullion_offset", (1, 0, 0)),
|
||||
GizmoPropConfig("second_mullion_offset", (1, 0, 0)),
|
||||
GizmoPropConfig("transom_thickness", (0, 0, 1), delta_scale=2.0),
|
||||
GizmoPropConfig("first_transom_offset", (0, 0, 1)),
|
||||
GizmoPropConfig("second_transom_offset", (0, 0, 1)),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
return tool.Blender.Modifier.is_window(element)
|
||||
|
||||
def get_props(self, obj):
|
||||
return tool.Model.get_window_props(obj)
|
||||
|
||||
def get_gizmo_prefs(self):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return prefs.gizmos.window
|
||||
|
||||
def should_hide_gizmo(self, attr_name, props):
|
||||
"""Window-specific visibility rules for gizmos."""
|
||||
if not props.is_editing:
|
||||
return True
|
||||
|
||||
has_mullion = self._has_mullion(props)
|
||||
has_second_mullion = self._has_second_mullion(props)
|
||||
has_transom = self._has_transom(props)
|
||||
has_second_transom = self._has_second_transom(props)
|
||||
|
||||
if attr_name == "mullion_thickness" and not has_mullion:
|
||||
return True
|
||||
if attr_name == "first_mullion_offset" and not has_mullion:
|
||||
return True
|
||||
if attr_name == "second_mullion_offset" and not has_second_mullion:
|
||||
return True
|
||||
if attr_name == "transom_thickness" and not has_transom:
|
||||
return True
|
||||
if attr_name == "first_transom_offset" and not has_transom:
|
||||
return True
|
||||
if attr_name == "second_transom_offset" and not has_second_transom:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_gizmo_matrix_overall_height(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_overall_width(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_lining_depth(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_lining_thickness(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.lining_thickness, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
@staticmethod
|
||||
def _get_lining_to_panel_offset_y_full(props) -> float:
|
||||
"""Get the full Y offset for lining-to-panel positioning."""
|
||||
return (props.lining_depth - props.frame_depth[0]) + props.lining_to_panel_offset_y
|
||||
|
||||
def get_gizmo_matrix_lining_to_panel_offset_x(self, props):
|
||||
y_full = self._get_lining_to_panel_offset_y_full(props)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.lining_to_panel_offset_x, y_full + props.lining_offset, props.lining_thickness)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_lining_to_panel_offset_y(self, props):
|
||||
y_full = self._get_lining_to_panel_offset_y_full(props)
|
||||
translation = Matrix.Translation(
|
||||
V_(
|
||||
props.lining_to_panel_offset_x,
|
||||
y_full + props.frame_depth[0] + props.lining_offset,
|
||||
props.lining_thickness,
|
||||
)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_mullion_thickness(self, props):
|
||||
# Position at the right edge of the mullion (offset + thickness/2) so gizmo movement matches property 1:1
|
||||
translation = Matrix.Translation(
|
||||
V_(props.first_mullion_offset + props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_first_mullion_offset(self, props):
|
||||
# Position at the left edge of the mullion (offset - thickness/2)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.first_mullion_offset - props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_second_mullion_offset(self, props):
|
||||
translation = Matrix.Translation(V_(props.second_mullion_offset, props.lining_offset, props.overall_height / 2))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_transom_thickness(self, props):
|
||||
# Position at the top edge of the transom (offset + thickness/2) so gizmo movement matches property 1:1
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset + props.transom_thickness / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_first_transom_offset(self, props):
|
||||
# Position at the bottom edge of the transom (offset - thickness/2)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset - props.transom_thickness / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_second_transom_offset(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width / 2, props.lining_offset, props.second_transom_offset))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def _has_mullion(self, props):
|
||||
"""Check if the window type uses mullions (vertical dividers)."""
|
||||
window_type = props.window_type
|
||||
return window_type in (
|
||||
"DOUBLE_PANEL_VERTICAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
"TRIPLE_PANEL_LEFT",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_VERTICAL",
|
||||
)
|
||||
|
||||
def _has_second_mullion(self, props):
|
||||
"""Check if the window type uses a second mullion."""
|
||||
return props.window_type == "TRIPLE_PANEL_VERTICAL"
|
||||
|
||||
def _has_transom(self, props):
|
||||
"""Check if the window type uses transoms (horizontal dividers)."""
|
||||
window_type = props.window_type
|
||||
return window_type in (
|
||||
"DOUBLE_PANEL_HORIZONTAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
"TRIPLE_PANEL_LEFT",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_HORIZONTAL",
|
||||
)
|
||||
|
||||
def _has_second_transom(self, props):
|
||||
"""Check if the window type uses a second transom."""
|
||||
return props.window_type == "TRIPLE_PANEL_HORIZONTAL"
|
||||
|
||||
def setup(self, context):
|
||||
# Use base class methods for common gizmos
|
||||
self.setup_property_gizmos(context)
|
||||
self.setup_editing_gizmos(context)
|
||||
|
||||
def refresh(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
props = self.get_props(obj)
|
||||
mw = obj.matrix_world
|
||||
self.update_property_gizmos(mw, props)
|
||||
self.update_editing_gizmos(context, mw, props)
|
||||
|
||||
@@ -1322,6 +1322,20 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
if self.active_material_usage == "LAYER2":
|
||||
if len(bpy.context.selected_objects) != 2:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Exactly two LAYER2 items (walls, railings, etc) must be selected to perform a merge.",
|
||||
)
|
||||
return
|
||||
for item in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(item)
|
||||
if tool.Model.get_usage_type(element) != "LAYER2":
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Both selected items must be LAYER2 (walls, railings, etc) to perform a merge.",
|
||||
)
|
||||
return
|
||||
bpy.ops.bim.merge_wall()
|
||||
else:
|
||||
if len(bpy.context.selected_objects) == 1:
|
||||
|
||||
@@ -635,6 +635,7 @@ class BIM_PT_purge(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_quality_control"
|
||||
|
||||
def draw(self, context):
|
||||
props = tool.Debug.get_debug_props()
|
||||
layout = self.layout
|
||||
layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE"
|
||||
layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE"
|
||||
@@ -655,3 +656,23 @@ class BIM_PT_purge(Panel):
|
||||
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
|
||||
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
|
||||
merge_op.object_type = object_type
|
||||
|
||||
layout.operator("bim.purge_unused_representations")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "ifc_class_purge", text="")
|
||||
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
|
||||
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
|
||||
|
||||
if context.active_object and (data := context.active_object.data):
|
||||
mprops = tool.Geometry.get_mesh_props(data)
|
||||
row = layout.row()
|
||||
row.operator("bim.get_representation_ifc_parameters")
|
||||
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
|
||||
row = layout.row(align=True)
|
||||
row.prop(ifc_parameter, "name", text="")
|
||||
row.prop(ifc_parameter, "value", text="")
|
||||
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -859,8 +859,23 @@ def get_gross_top_area(obj: bpy.types.Object, angle: float = 45) -> float:
|
||||
|
||||
entity = ifc.by_guid(opening_id)
|
||||
open_obj = tool.Ifc.get_object(entity)
|
||||
assert isinstance(open_obj, bpy.types.Object)
|
||||
opening_area += get_net_top_area(open_obj, angle=angle)
|
||||
|
||||
# Check if the opening has a Blender object representation
|
||||
if open_obj is None:
|
||||
# If no Blender object exists, create a temporary mesh from the IFC geometry
|
||||
try:
|
||||
mesh = get_gross_element_mesh(entity)
|
||||
temp_obj = bpy.data.objects.new("TempOpening", mesh)
|
||||
opening_area += get_net_top_area(temp_obj, angle=angle)
|
||||
delete_obj(temp_obj)
|
||||
delete_mesh(mesh)
|
||||
except Exception as e:
|
||||
# If we can't create geometry, skip this opening
|
||||
print(f"Warning: Could not calculate opening area for {opening_id}: {e}")
|
||||
continue
|
||||
else:
|
||||
# Opening has a Blender representation, use it directly
|
||||
opening_area += get_net_top_area(open_obj, angle=angle)
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifcopenshell.api
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.qto as core
|
||||
from bonsai.bim.module.qto import helper
|
||||
from ifcopenshell.util.profiler import Profiler
|
||||
|
||||
|
||||
class CalculateCircleRadius(bpy.types.Operator):
|
||||
@@ -148,7 +149,8 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
|
||||
}
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
with Profiler("Quantify function time:"):
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
ifc5d.qto.edit_qtos(ifc_file, results)
|
||||
|
||||
not_quantified_elements = elements - set(results.keys())
|
||||
@@ -190,7 +192,8 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
|
||||
) -> set[ifcopenshell.entity_instance]:
|
||||
rules = ifc5d.qto.rules[rule]
|
||||
ifc_file = tool.Ifc.get()
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
with Profiler("Quantify function time:"):
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
ifc5d.qto.edit_qtos(ifc_file, results)
|
||||
not_quantified_elements = elements - set(results.keys())
|
||||
return not_quantified_elements
|
||||
|
||||
@@ -21,6 +21,7 @@ import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.spatial as core
|
||||
import bonsai.bim.handler
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -233,7 +234,7 @@ class RemoveContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"""
|
||||
Copies selected 3D elements in the viewport to the selected spatial containers
|
||||
Copies selected 3D elements in the viewport to the container selected in Spatial Manager.
|
||||
|
||||
Example: bulk copy a wall to multiple storeys
|
||||
|
||||
@@ -242,9 +243,13 @@ class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
Copying containers to other containers currently is not supported."""
|
||||
|
||||
bl_idname = "bim.copy_to_container"
|
||||
bl_label = "Copy To Container"
|
||||
bl_label = "Copy to Container"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
container: bpy.props.IntProperty()
|
||||
|
||||
container: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
container: int
|
||||
|
||||
def _execute(self, context):
|
||||
objs = tool.Spatial.get_selected_objects_without_containers()
|
||||
|
||||
@@ -36,6 +36,7 @@ import bonsai.core.geometry
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.attribute
|
||||
import ifcopenshell.util.unit
|
||||
from bonsai.tool.unit import parse_distance_string
|
||||
from typing import TYPE_CHECKING, Union, Literal
|
||||
|
||||
|
||||
@@ -48,18 +49,46 @@ def get_subelement_class(
|
||||
|
||||
|
||||
def update_elevation(self: "BIMContainer", context: bpy.types.Context) -> None:
|
||||
try:
|
||||
elevation = float(self.elevation)
|
||||
if self.elevation != str(elevation):
|
||||
self.elevation = str(elevation)
|
||||
return
|
||||
except:
|
||||
elevation = 0
|
||||
# Try to parse the input string with unit support first
|
||||
is_valid, parsed_elevation = parse_distance_string(self.elevation, use_project_unit=True)
|
||||
|
||||
if is_valid:
|
||||
elevation = parsed_elevation
|
||||
|
||||
# Format the string if it needs normalization:
|
||||
# - Plain numbers (no units): "4" → "4' - 0""
|
||||
# - Missing symbols: "5'3" → "5' - 3""
|
||||
# - Inconsistent formatting: "5' 3 3/256" → "5' - 3 3/256""
|
||||
input_str = self.elevation.strip()
|
||||
|
||||
# Check if input needs formatting (has feet/inch symbols or is plain number)
|
||||
needs_formatting = (
|
||||
input_str.replace(".", "").replace("-", "").replace(" ", "").replace("/", "").isdigit() # Plain number
|
||||
or "'" in input_str # Has feet symbol
|
||||
or '"' in input_str # Has inch symbol
|
||||
or " " in input_str.replace(" - ", "") # Has spaces (but not the formatted " - ")
|
||||
)
|
||||
|
||||
# Only format if the current string doesn't match our standard format
|
||||
if needs_formatting:
|
||||
formatted = tool.Unit.format_distance(elevation)
|
||||
if self.elevation != formatted:
|
||||
self.elevation = formatted
|
||||
return
|
||||
else:
|
||||
# Fall back to direct float conversion for backward compatibility
|
||||
try:
|
||||
elevation = float(self.elevation)
|
||||
except Exception as e:
|
||||
print(f"Elevation parsing failed for '{self.elevation}': {e}")
|
||||
elevation = 0
|
||||
|
||||
# Update the object's position in the 3D scene
|
||||
if ifc_definition_id := self.ifc_definition_id:
|
||||
entity = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
if obj := tool.Ifc.get_object(entity):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
obj.matrix_world[2][3] = elevation * unit_scale
|
||||
# elevation is already in meters, set it directly
|
||||
obj.matrix_world[2][3] = elevation
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj)
|
||||
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ class BIM_UL_containers_manager(UIList):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
icon = self.icon_by_class.get(item.ifc_class, "META_PLANE")
|
||||
split = row.split(factor=0.85)
|
||||
split = row.split(factor=0.8)
|
||||
if item.long_name:
|
||||
split2 = split.split(factor=0.7)
|
||||
row = split2.row(align=True)
|
||||
|
||||
@@ -455,7 +455,7 @@ class ActivateExternalStyle(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.copy_material_attributes(db["data_block"], material)
|
||||
if material.use_nodes:
|
||||
if tool.Style.get_use_nodes(material):
|
||||
tool.Blender.copy_node_graph(material, db["data_block"])
|
||||
bpy.data.materials.remove(db["data_block"])
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -99,7 +99,8 @@ class BIMSystemProperties(PropertyGroup):
|
||||
systems: CollectionProperty(name="Systems", type=System)
|
||||
expanded_groups_json: StringProperty(name="Expanded Systems JSON", default="[]")
|
||||
"""See `expanded_groups_json`. We name it "groups" for compatibility with Group UI code."""
|
||||
active_system_index: IntProperty(name="Active System Index")
|
||||
# Named not as `active_system_index` to match `BIMGroupProperties`.
|
||||
active_group_index: IntProperty(name="Active System Index")
|
||||
active_system_id: IntProperty(name="Active System Id")
|
||||
edited_system_id: IntProperty(name="Edited System Id")
|
||||
system_class: EnumProperty(items=get_system_class, name="Class")
|
||||
@@ -113,7 +114,7 @@ class BIMSystemProperties(PropertyGroup):
|
||||
is_adding: bool
|
||||
systems: bpy.types.bpy_prop_collection_idprop[System]
|
||||
expanded_groups_json: str
|
||||
active_system_index: int
|
||||
active_group_index: int
|
||||
active_system_id: int
|
||||
edited_system_id: int
|
||||
system_class: str
|
||||
@@ -121,7 +122,7 @@ class BIMSystemProperties(PropertyGroup):
|
||||
|
||||
@property
|
||||
def active_system_ui_item(self) -> Union[System, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.systems, self.active_system_index)
|
||||
return tool.Blender.get_active_uilist_element(self.systems, self.active_group_index)
|
||||
|
||||
|
||||
def update_active_zone_index(self: "BIMZoneProperties", context: object) -> None:
|
||||
|
||||
@@ -114,7 +114,7 @@ class BIM_PT_systems(Panel):
|
||||
self.props,
|
||||
"systems",
|
||||
self.props,
|
||||
"active_system_index",
|
||||
"active_group_index",
|
||||
)
|
||||
|
||||
if self.props.edited_system_id:
|
||||
|
||||
@@ -322,6 +322,15 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Duplicate Type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
element: bpy.props.IntProperty()
|
||||
name: bpy.props.StringProperty(name="Name")
|
||||
description: bpy.props.StringProperty(name="Description")
|
||||
assign_selected_objects: bpy.props.BoolProperty(default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
element: int
|
||||
name: str
|
||||
description: str
|
||||
assign_selected_objects: bool
|
||||
|
||||
def _execute(self, context):
|
||||
element = tool.Ifc.get().by_id(self.element)
|
||||
@@ -332,8 +341,25 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
new.Name += " Copy"
|
||||
|
||||
# Apply the name and description from the dialog
|
||||
new.Name = self.name
|
||||
if self.description:
|
||||
new.Description = self.description
|
||||
|
||||
bpy.ops.bim.load_type_thumbnails()
|
||||
|
||||
# Assign selected objects to the new type if requested
|
||||
if self.assign_selected_objects:
|
||||
selected_objects = tool.Blender.get_selected_objects()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
for selected_obj in selected_objects:
|
||||
selected_element = tool.Ifc.get_entity(selected_obj)
|
||||
if selected_element and selected_element.is_a("IfcObject"):
|
||||
core.assign_type(tool.Ifc, tool.Type, element=selected_element, type=new)
|
||||
if prefs.occurrence_name_style == "TYPE":
|
||||
selected_obj.name = tool.Model.generate_occurrence_name(new, selected_element.is_a())
|
||||
|
||||
if obj in context.selectable_objects:
|
||||
tool.Blender.select_and_activate_single_object(context, new_obj)
|
||||
else:
|
||||
@@ -347,3 +373,17 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.ifc_class = new.is_a()
|
||||
props.relating_type_id = str(tool.Blender.get_ifc_definition_id(new_obj))
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
element = tool.Ifc.get().by_id(self.element)
|
||||
self.name = (element.Name or "Unnamed") + " Copy"
|
||||
self.description = element.Description or ""
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.prop(self, "name")
|
||||
self.layout.prop(self, "description")
|
||||
selected_objects = tool.Blender.get_selected_objects()
|
||||
ifc_objects = [obj for obj in selected_objects if tool.Ifc.get_entity(obj) and tool.Ifc.get_entity(obj).is_a("IfcObject")]
|
||||
if ifc_objects:
|
||||
self.layout.prop(self, "assign_selected_objects", text=f"Assign {len(ifc_objects)} Selected Object(s) to New Type")
|
||||
|
||||
@@ -96,6 +96,10 @@ class BIM_PT_type(Panel):
|
||||
op = row.operator("bim.select_type", icon="OBJECT_DATA", text="")
|
||||
op.relating_type = 0 # will only select the relating types of only the selected objects
|
||||
row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="")
|
||||
# Add duplicate button here with assign_selected_objects enabled
|
||||
op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="")
|
||||
op.element = TypeData.data["relating_type"]["id"]
|
||||
op.assign_selected_objects = True
|
||||
row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.unassign_type", icon="X", text="")
|
||||
else:
|
||||
|
||||
@@ -102,6 +102,19 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element_had_openings = tool.Geometry.has_openings(voided_element)
|
||||
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body")
|
||||
if not element2:
|
||||
# Apply scale transformation for vanilla Blender objects before creating IFC representation
|
||||
# This ensures the opening geometry matches the visual scale of the object
|
||||
if tool.Geometry.is_scaled(obj2):
|
||||
if obj2.data:
|
||||
# Make mesh data single-user if it's shared
|
||||
if obj2.data.users > 1:
|
||||
obj2.data = obj2.data.copy()
|
||||
context_override = {}
|
||||
context_override["object"] = context_override["active_object"] = obj2
|
||||
context_override["selected_objects"] = context_override["selected_editable_objects"] = [obj2]
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
|
||||
element2 = bonsai.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user