Compare commits

..

57 Commits

Author SHA1 Message Date
Thomas Krijnen 2fe491b7b5 Merge remote-tracking branch 'origin/v0.8.0' into test-ci 2025-10-27 06:54:10 +01:00
Thomas Krijnen 83f7416679 Revert "[TMP] start a RelWithDebInfo build"
This reverts commit 9c8f3be52a.
2025-10-26 12:04:49 +01:00
Thomas Krijnen dc5ac872e3 Revert "[TEMP] IFCOS_SCHEMAS=4 probably disk is full with RelWithDebInfo"
This reverts commit e05318162d.
2025-10-26 12:04:46 +01:00
Thomas Krijnen e05318162d [TEMP] IFCOS_SCHEMAS=4 probably disk is full with RelWithDebInfo 2025-10-26 11:52:52 +01:00
Thomas Krijnen 9c8f3be52a [TMP] start a RelWithDebInfo build 2025-10-26 10:11:40 +01:00
Thomas Krijnen c4a992a9b5 Merge remote-tracking branch 'origin/v0.8.0' into test-ci 2025-10-26 09:50:01 +01:00
Andrej730 6d4f45c586 ifcwrap cmake - fix issue with debug build after migration to FindPython
In FindPython variable was renamed to Python_LIBRARY_DEBUG
2025-10-24 13:53:22 +05:00
Andrej730 0f9030e33b ifcwrap cmake - avoid using deprecated swig_link_libraries 2025-10-24 13:28:33 +05:00
Andrej730 ab0b44c8a2 ifcwrap cmake - use usual name for wrapper target 2025-10-24 12:52:37 +05:00
Andrej730 37e9a0f4fe cache_dependencies.py - fix tarfile deprecation warnings in 3.12-3.13 2025-10-24 12:33:44 +05:00
Andrej730 0787eabdab ifcwrap cmake - move DEBUG_POSTFIX logic to one place 2025-10-23 20:03:12 +05:00
Andrej730 0a34a37d65 cmake - build without Python libraries on Unix 2025-10-23 20:02:03 +05:00
Andrej730 280976f77c ifcwrap cmake - accomodate bug in cmake makefiles generator 2025-10-23 17:15:17 +05:00
Andrej730 4c53f4359d build-all - use shutil.which instead of custom function
To make it more idiomatic and custom function also was misinterpreting folders with the same name as existing executables.
2025-10-23 17:15:17 +05:00
Andrej730 0a547a14ea build_pyodide - use ccache 2025-10-23 17:15:17 +05:00
Andrej730 206ec8776a build_rocky - switch to main branch ccache 2025-10-23 17:15:16 +05:00
Andrej730 c3b43d573e build_pyodide - use prebuilt pyodide build env 2025-10-23 16:18:10 +05:00
Andrej730 afb6befce6 build-all.py - switch to gmp ftpmirror
previous url is inaccessible
2025-10-22 13:53:17 +05:00
Andrej730 f42efbade2 build_pyodide - display logs in github action output 2025-10-22 13:53:17 +05:00
Andrej730 968aa710ee build_pyodide - upload build logs for debugging 2025-10-22 13:53:17 +05:00
Andrej730 61b03dc60c build_pyodide - pack cached dependencies 2025-10-22 13:53:16 +05:00
Andrej730 e87597b8d7 build_osx - remove setup-python
as it's already preinstalled on the image
2025-10-22 10:37:45 +05:00
Andrej730 b12dddc2f0 build-all.py - print BUILD_DIR 2025-10-22 10:37:44 +05:00
Andrej730 f3076ba038 build_pyodide - no need to adjust version
since it's now inferred automatically
2025-10-22 10:37:44 +05:00
Andrej730 908a001ed8 build-all.py - accomodate pyodide build system automatically
as it's the main way to build the wasm package
2025-10-22 10:37:44 +05:00
Andrej730 1a8c47888a build_pyodide.yaml - move build_pyodide to a separate script 2025-10-22 10:37:44 +05:00
Andrej730 5e46a759a6 Add notes for pyodide building process 2025-10-22 10:37:44 +05:00
Andrej730 0e3f8cba8d build-all.py wasm - move SIDE_MODULE=2 to cmake
Next to `EXPORTED_FUNCTIONS` and avoid possible issues with pyodide in the future (pyodide is using `SIDE_MODULE=1`, but currently not providing it libraries of type `MODULE`, only for shared ones).
2025-10-22 10:37:43 +05:00
Andrej730 0ce12c9297 ifcwrap cmake - provide correct suffix for wasm python extension 2025-10-22 10:37:43 +05:00
Andrej730 ee8433655d pyodide setup.py - add dependencies
so micropip would be able to gather them automatically
2025-10-20 18:36:38 +05:00
Andrej730 29434d623c pyodide setup.py - add a note on not switching to pyproject.toml 2025-10-17 19:26:50 +05:00
Andrej730 092705ae68 pyodide setup.py - ensure we have correct pyodide_2025_0_wasm32 suffix
Adding extension, but avoiding adding sources as we're not actually building.
As a result build will have a correct suffix `pyodide_2025_0_wasm32.whl` that's pyodide is currently using and this is pyodide is plan to use when it's going to release to PyPI, see https://peps.python.org/pep-0783/
2025-10-17 19:26:50 +05:00
Andrej730 5f777691cf build-all.py - add a note about alternative gmp mirror 2025-10-17 17:16:36 +05:00
Andrej730 46913422b3 cmake - deduplicate ccache set_property code 2025-10-17 15:34:11 +05:00
Andrej730 ebcdabff07 build-all.py - don't pass side module flags as usual flags
As they're parsed by emcmake automatically https://github.com/pyodide/pyodide-build/blob/bdd0b1422064de6d92264a08c599e85001a28567/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake#L105-L107
2025-10-17 15:34:11 +05:00
Andrej730 2cfa5201ca build-all.py - option to build wasm outside pyodide for debugging 2025-10-16 11:18:41 +05:00
Andrej730 8dafcc6b88 build-all.py - provide exported functions to target explicitly
Otherwise it broke compilation in some cases when cmake would try to test that linker is working.
2025-10-16 11:18:41 +05:00
Andrej730 626bb8b011 build-all - wasm to use packages instead of providing includes/libs paths explicitly
More complicated than I anticipated😅

Details
https://discourse.cmake.org/t/provide-absolute-paths-to-dependencies-with-cmake-find-root-path-search-during-cross-compilation-wasm/15241
2025-10-16 11:18:41 +05:00
Andrej730 721f74f045 build-all.py - move unsupported dependencies to script 2025-10-16 11:18:40 +05:00
Andrej730 31ace7c941 build-deps - use nuget to download ccache 2025-10-13 16:55:49 +05:00
Andrej730 612bd0b0a0 build-all.py - print IFCOS_SCHEMAS 2025-10-10 20:46:03 +05:00
Andrej730 3414d3c7ef build-all.py - document required wasm variables 2025-10-10 20:46:03 +05:00
Andrej730 6c1b3bcbdc build-all.py - fix issue building wasm on apple device 2025-10-10 17:48:29 +05:00
Andrej730 ceba8d51c5 build-all.py - remove --host none from wasm args
As it's now obsolete and producing warnings `configure: WARNING: the "none" host is obsolete, use --disable-assembly`.
2025-10-10 17:48:28 +05:00
Andrej730 0c3fbfa2b8 build-all.py - bump freetype to avoid issues on cmake 4+ 2025-10-10 17:48:28 +05:00
Andrej730 05805bb557 build-all.py - add note on --without-xxx option 2025-10-10 17:48:28 +05:00
Andrej730 3edb006c35 Add Python 3.14 builds 2025-10-09 17:13:07 +05:00
Andrej730 b36cd6442e build-deps - set NUGET_EXE from PATH if it's available 2025-10-08 18:37:15 +05:00
Andrej730 fbd6755666 build-all - deduce python components from python path 2025-10-08 18:37:15 +05:00
Andrej730 ffdee45cf9 ifcwrap cmake - migrate to FindPython
Also simplify run-cmake to just prefix the path, instead of providing 3 paths explicitly.
2025-10-08 17:16:34 +05:00
Andrej730 51bf4951b3 build-deps - support overriding PYTHON_VERSION 2025-10-07 11:50:43 +05:00
Andrej730 9c8ae71323 build-deps - download Python using nuget 2025-10-06 19:59:47 +05:00
Andrej730 8a95ec130f build-deps - download nuget
to later download python using it
2025-10-06 19:59:46 +05:00
Andrej730 b2f5e4b11c build-deps - include ccache for convenience 2025-10-06 19:59:46 +05:00
Andrej730 91a2504629 build_win - extend cache size 2025-10-06 19:59:46 +05:00
Andrej730 1eaf110d3d build_win - use fork to resolve cache misses and duplicated cache 2025-10-06 19:59:46 +05:00
Andrej730 9688db67b6 build_win - test ccache 2025-10-06 19:59:46 +05:00
204 changed files with 1960 additions and 4612 deletions
-8
View File
@@ -1,8 +0,0 @@
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+4 -4
View File
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -81,7 +81,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: build-logs-osx-${{ matrix.arch }}
path: |
@@ -139,7 +139,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+4 -17
View File
@@ -9,13 +9,13 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
path: IfcOpenShell
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ifcopenshell_build
@@ -42,26 +42,13 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: build-logs-pyodide
path: |
ifcopenshell_build/*/*/logs/*.log
retention-days: 30
- name: Run wheel tests
run: |
cp -r IfcOpenShell/pyodide/test test
# venv set up in build_pyodide.sh.
source .venv/bin/activate
uv pip install pytest-pyodide
PYODIDE_ROOT_DIST=`pyodide config get pyodide_root`/dist
# `pytest-pyodide` requires pyodide in 'pyodide' directory in cwd, when running `pytest`.
cp -r $PYODIDE_ROOT_DIST test/pyodide
cp dist/ifcopenshell-*.whl test/pyodide
cd test
pytest --capture=no
- name: Pack Dependencies
run: |
cd ifcopenshell_build
@@ -77,7 +64,7 @@ jobs:
git push || echo "Push failed"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+6 -6
View File
@@ -29,12 +29,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -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
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
@@ -61,7 +61,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: build-logs-rocky
path: |
@@ -118,7 +118,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+6 -6
View File
@@ -29,12 +29,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -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
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
@@ -61,7 +61,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: build-logs-rocky-arm64
path: |
@@ -118,7 +118,7 @@ jobs:
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+5 -5
View File
@@ -12,12 +12,12 @@ jobs:
arch: ['x64']
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: _deps-vs2022-x64-installed
@@ -37,8 +37,8 @@ jobs:
}
- name: ccache
# TODO: Use tag after 1.2.20 releases.
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
# Use fork to resolve cache misses / duplicated cache entries on Windows.
uses: Andrej730/ccache-action@main
with:
key: win-${{ matrix.arch }}
# Windows ccache needs ~1GB
@@ -73,7 +73,7 @@ jobs:
git push || echo "Push failed"
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5 # https://github.com/actions/checkout
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2 # https://github.com/actions/checkout
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+7 -9
View File
@@ -9,15 +9,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Action - checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4.2.2
- name: Action - install python
uses: actions/setup-python@v6
uses: actions/setup-python@v5.3.0
with:
python-version: "3.9"
- name: Action - install python
uses: actions/setup-python@v6
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
@@ -26,7 +26,6 @@ jobs:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install ruff
uv tool install black
uv tool install poethepoet
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
@@ -41,16 +40,15 @@ jobs:
- name: Black formatter
id: black
run: |
black --diff --check .
uvx black --diff --check .
continue-on-error: true
- name: Ruff check
id: ruff
run: |
ERROR=0
poe ruff-main || ERROR=1
poe ruff-old || ERROR=1
exit $ERROR
uvx ruff check
# It's actually Python 3.6, but ruff only supports 3.7+, but it should do.
uvx ruff check nix/build-all.py --target-version py37
continue-on-error: true
- name: Final check
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
+3 -3
View File
@@ -54,8 +54,8 @@ jobs:
short_name: macosm1,
}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
@@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout bonsai_unstable_repo repository
uses: actions/checkout@v5
uses: actions/checkout@v2
with:
repository: IfcOpenShell/bonsai_unstable_repo
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+2 -2
View File
@@ -43,8 +43,8 @@ jobs:
short_name: macosm1,
}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -37,8 +37,8 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
@@ -24,7 +24,7 @@ jobs:
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
@@ -21,7 +21,7 @@ jobs:
date: ${{ steps.date.outputs.date }}
verdate: ${{ steps.verdate.outputs.verdate }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set env
run: echo ok go
@@ -76,7 +76,7 @@ jobs:
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
submodules: recursive
+8 -8
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
@@ -76,7 +76,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
# Artifact name
name: ifcos-artifacts
@@ -89,31 +89,31 @@ jobs:
name: Docker Build, Tag, Push
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
lfs: true
- name: Download
uses: actions/download-artifact@v6.0.0
uses: actions/download-artifact@v4.1.7
with:
# Artifact name
name: ifcos-artifacts
path: artifacts/
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v1
-
name: Login to Dockerhub
uses: docker/login-action@v3
uses: docker/login-action@v1
with:
username: aecgeeks
password: ${{ secrets.DOCKER_HUB_TOKEN }}
-
name: Build container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v2
with:
context: artifacts
repository: aecgeeks/ifcopenshell
@@ -34,10 +34,6 @@ jobs:
name: "Linux 64bit",
short_name: linux64
}
- {
name: "Linux ARM 64bit",
short_name: linuxarm64
}
- {
name: "MacOS Intel 64bit",
short_name: macos64
@@ -47,10 +43,10 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -38,10 +38,10 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+2 -2
View File
@@ -25,8 +25,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
publish_website:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
- name: Checkout ifctester_org_static_html
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: IfcOpenShell/ifctester_org_static_html
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
+7 -45
View File
@@ -35,12 +35,12 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.11
@@ -64,22 +64,17 @@ jobs:
libboost-regex-dev \
libboost-system-dev \
libboost-thread-dev \
libpcre3-dev libxml2-dev \
swig libpcre3-dev libxml2-dev \
libtbb-dev nlohmann-json3-dev \
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
libhdf5-dev libcgal-dev libeigen3-dev
- name: ccache
# TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released
# to update hardcoded references to commits in some other workflows.
# Then we can switch back to 1.2 in all actions.
uses: hendrikmuhs/ccache-action@v1.2.19
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ubuntu-22.04-${{ runner.arch }}
# RTTI is only enabled by default in Debug builds of rocksdb.
# Distros are using Release builds, so we're compiling it ourselves with RTTI forced on.
# https://github.com/facebook/rocksdb/blob/a3aa44a7167b8336f9bc15c8aba063260268ff68/CMakeLists.txt#L433
# rocksdb on debian distros misses RTTI?
- name: build rocksdb
run: |
git clone https://github.com/facebook/rocksdb --branch v9.11.2
@@ -100,41 +95,6 @@ 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
@@ -152,6 +112,8 @@ 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@v5
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v2
with:
python-version: '3.x'
+2 -2
View File
@@ -9,10 +9,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v2
with:
python-version: '3.x'
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
@@ -34,7 +34,7 @@ jobs:
uses: actions/configure-pages@v5
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v3
with:
path: src/pyodide/demo-app/
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install C++ dependencies
+5 -2
View File
@@ -17,7 +17,6 @@
# General Python residue
__pycache__
*.py.bak
venv
# Visual Studio Code files
.vscode
@@ -79,6 +78,8 @@ src/bonsai/bonsai/translations.py
# bonsai test temp files
src/bonsai/test/files/temp
src/bonsai/test/files/basic.ifc.cache.blend
src/bonsai/test/files/basic.ifc.cache.sqlite
# bonsai data
src/bonsai/bonsai/bim/data/build/
@@ -86,7 +87,9 @@ src/bonsai/bonsai/bim/data/gantt/index.html
src/bonsai/bonsai/bim/data/gantt/jsgantt.js
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
src/bonsai/bonsai/bim/data/webui/static/js/jquery.min.js
src/bonsai/bonsai/bim/data/webui/running_pid.json
src/bonsai/drawings
src/bonsai/layouts
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
+3
View File
@@ -11,6 +11,9 @@
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
path = src/ifcopenshell-python/test/Sample-BIM-Files
url = https://github.com/IfcOpenShell/ids-test-files
[submodule "src/ifcconvert/cityjson"]
path = src/ifcconvert/cityjson
url = https://github.com/IfcOpenShell/ifc-to-cityjson
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
path = docs/cpp-api/assets/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git
+1 -1
View File
@@ -52,7 +52,7 @@ Contents
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) |
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
+522 -19
View File
@@ -79,6 +79,7 @@ option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
option(CITYJSON_SUPPORT "Build IfcConvert with CityJSON support (requires CityJSON library)." OFF)
option(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)
@@ -103,9 +104,6 @@ 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)
@@ -198,6 +196,8 @@ 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)
@@ -259,7 +259,7 @@ if(WITH_CGAL)
else()
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
endif()
find_package(CGAL REQUIRED)
find_package(CGAL REQUIRED)
if(NOT CGAL_DIR)
message(
FATAL_ERROR
@@ -273,6 +273,10 @@ if(WITH_CGAL)
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()
@@ -282,7 +286,7 @@ if(WITH_OPENCASCADE)
list(APPEND GEOMETRY_KERNELS opencascade)
endif()
if(GLTF_SUPPORT)
if(GLTF_SUPPORT OR CITYJSON_SUPPORT)
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
if(NOT JSON_INCLUDE_DIR)
find_package(nlohmann_json CONFIG)
@@ -312,7 +316,49 @@ endif()
# Add USD support to serializers
if(USD_SUPPORT)
find_package(USD REQUIRED)
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)
endif(USD_SUPPORT)
if (WITH_ROCKSDB)
@@ -404,7 +450,32 @@ if(NOT MINIMAL_BUILD)
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
clear_wasm_sysroot()
if(IFCXML_SUPPORT)
find_package(LibXml2 REQUIRED)
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
# CONFIG is provided using root path, so no need to clear sysroot here.
restore_wasm_sysroot()
find_package(LibXml2 REQUIRED CONFIG)
clear_wasm_sysroot()
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()
endif()
restore_wasm_sysroot()
endif()
@@ -421,7 +492,141 @@ if(BUILD_IFCGEOM)
# Open CASCADE
if(WITH_OPENCASCADE)
find_package(OpenCASCADE REQUIRED)
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()
endif(WITH_OPENCASCADE)
endif(BUILD_IFCGEOM)
@@ -525,7 +730,91 @@ endif()
if(HDF5_SUPPORT)
find_package(HDF5 REQUIRED COMPONENTS C CXX)
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()
add_definitions(-DWITH_HDF5)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
@@ -659,7 +948,7 @@ endif()
include_directories(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS}
${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${HDF5_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}
${TBB_INCLUDE_DIR}
)
@@ -749,7 +1038,6 @@ 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)
@@ -760,13 +1048,18 @@ if(BUILD_IFCGEOM)
endif()
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
if(WITH_OPENCASCADE)
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)
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)
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()
@@ -777,6 +1070,52 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
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)
@@ -798,8 +1137,33 @@ if(BUILD_IFCGEOM)
list(APPEND CGAL_LIBRARIES "${libGMP}")
endif()
add_subdirectory(../src/ifcgeom ifcgeom)
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()
# IfcGeom
foreach(schema ${SCHEMA_VERSIONS})
file(GLOB IFCGEOM_I_FILES ../src/ifcgeom/mapping/*.i)
@@ -833,17 +1197,129 @@ if(BUILD_IFCGEOM)
endif(BUILD_IFCGEOM)
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
add_subdirectory(../src/serializers serializers)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} ${SERIALIZER_SCHEMA_LIBRARIES})
# 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)
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
if(BUILD_CONVERT)
add_subdirectory(../src/ifcconvert ifcconvert)
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}
)
endif(BUILD_CONVERT)
# IfcGeomServer
if(BUILD_GEOMSERVER)
add_subdirectory(../src/ifcgeomserver ifcgeomserver)
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}
)
endif(BUILD_GEOMSERVER)
if(ADD_COMMIT_SHA)
@@ -930,6 +1406,17 @@ 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
@@ -958,6 +1445,22 @@ 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}
+1
View File
@@ -17,6 +17,7 @@
"IFCXML_SUPPORT": "ON",
"HDF5_SUPPORT": "ON",
"SCHEMA_VERSIONS": "4x3_add2",
"CITYJSON_SUPPORT": "OFF",
"CMAKE_GENERATOR_PLATFORM": "",
"CMAKE_GENERATOR_TOOLSET": ""
}
-44
View File
@@ -1,44 +0,0 @@
#
# Input variables:
# - `LIBXML2_INCLUDE_DIR`
# - `LIBXML2_LIBRARIES`
# If input variables are not specified, try to find LibXml2 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `LIBXML2_INCLUDE_DIR`
# - `LIBXML2_LIBRARIES`
#
# To avoid cyclic calls to this file
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
# CONFIG is provided using root path, so no need to clear sysroot here.
restore_wasm_sysroot()
find_package(LibXml2 QUIET CONFIG)
clear_wasm_sysroot()
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()
# Restore module path.
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
-151
View File
@@ -1,151 +0,0 @@
#
# Input variables:
# - `OCC_INCLUDE_DIR`
# - `OCC_LIBRARY_DIR`
# If input variables are not specified, try to find OpenCASCADE config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `OCC_INCLUDE_DIR`
# - `OCC_LIBRARY_DIR`
# - `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()
# 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_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)
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_LIBRARIES})
# 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()
-54
View File
@@ -1,54 +0,0 @@
#
# Input variables:
# - `USD_INCLUDE_DIR`
# - `USD_LIBRARY_DIR`
# Input variables could also be provided as environment variables.
# TODO: Try to find USD config if varibales are not provided.
# TODO: does usd have a config file?
#
# Output variables:
# - `USD_LIBRARIES`
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
if("${USD_INCLUDE_DIR}" STREQUAL "")
find_path(USD_INCLUDE_DIR pxr.h
PATHS
/usr/include/pxr
/usr/local/include/pxr
REQUIRED
)
if(USD_INCLUDE_DIR)
message(STATUS "Found USD include files in: ${USD_INCLUDE_DIR}")
else()
message(FATAL_ERROR "Unable to find USD include directory, specify USD_INCLUDE_DIR manually.")
endif()
else()
set(USD_INCLUDE_DIR ${USD_INCLUDE_DIR} CACHE FILEPATH "USD header files")
message(STATUS "Looking for USD include files in: ${USD_INCLUDE_DIR}")
endif()
set(USD_LIBRARIES
usd_usd
usd_usdGeom
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_gf
)
find_library(USD_LIBRARY
NAMES ${USD_LIBRARIES}
PATHS ${USD_LIBRARY_DIR})
if(USD_LIBRARY)
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
link_directories(${USD_LIBRARY_DIR})
else()
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
endif()
add_definitions(-DWITH_USD)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
-91
View File
@@ -1,91 +0,0 @@
# 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})
+1
View File
@@ -42,6 +42,7 @@ cmake -G "Ninja" ^
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
-D CITYJSON_SUPPORT:BOOL=OFF ^
../cmake
if errorlevel 1 exit 1
+1
View File
@@ -42,6 +42,7 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DBUILD_IFCGEOM:BOOL=ON \
-DBUILD_GEOMSERVER:BOOL=OFF \
-DBOOST_USE_STATIC_LIBS:BOOL=OFF \
-DCITYJSON_SUPPORT:BOOL=OFF \
./cmake
ninja
+41 -70
View File
@@ -18,6 +18,8 @@
# #
###############################################################################
# ruff: noqa: UP035
"""
Example usage:
# Build all targets by default.
@@ -208,53 +210,34 @@ 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
WASM_DEBUG = 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")
if "WASM_PYTHON_PATH" in os.environ:
wasm_python_path = os.environ["WASM_PYTHON_PATH"]
# Deduce version from path, assuming format .../python-X.Y.Z
version_match = re.search(r"python-(\d+)\.(\d+)\.(\d+)", wasm_python_path)
assert version_match, f"Could not deduce python version from '{wasm_python_path}'"
python_version = version_match.group(1)
os.environ["PYVERSION"] = python_version
os.environ["PYTHONINCLUDE"] = f"{wasm_python_path}/include/python{python_version.rpartition('.')[0]}"
os.environ["SIDE_MODULE_CFLAGS"] = ""
# Required, otherwise library will compile as .a, not .so.
os.environ["SIDE_MODULE_LDFLAGS"] = "-s SIDE_MODULE=1"
# We're outside pyodide build environment, so need to provide toolchain file ourselves.
assert "WASM_TOOLCHAIN_FILE" in os.environ, "WASM_TOOLCHAIN_FILE must be set when WASM_PYTHON_PATH is provided"
WASM_DEBUG = True
required_vars = (
# E.g. '3.13.2'.
"PYVERSION",
# 'include' folder in WASM-Python installation.
# e.g. '/pyodide/cpython/installs/python-3.13.2/include/python3.13'
"PYTHONINCLUDE",
"SIDE_MODULE_CFLAGS",
"SIDE_MODULE_LDFLAGS",
"CMAKE_TOOLCHAIN_FILE",
)
missing_vars = [v for v in required_vars if v not in os.environ]
assert not missing_vars, f"Some variables required for WASM compilation are missing: {', '.join(missing_vars)}"
def get_pyodide_build_version() -> "tuple[int, ...]":
pyodide_build_suffix = "pyodide-build version:"
output = sp.check_output(["pyodide", "--version"], encoding="utf-8").strip()
assert pyodide_build_suffix in output, output
version_line = next(l for l in output.splitlines() if l.startswith(pyodide_build_suffix))
version = version_line.partition(":")[2].strip()
return tuple(map(int, version.split(".")))
# Pyodide still in transition from `FLAGS` to `FLAGS_INIT`.
# `FLAGS_INIT` allow us to provide flags using environment variables
# and providing `FLAGS` directly would break pyodide toolchain.
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (0, 30, 8)
# pyodide provide empty `CXXFLAGS`, leading to issues using C++ files compiled with `-fexceptions`
# which is used by OCCT.
# https://github.com/pyodide/pyodide-build/issues/251
side_module_cxx_flags = os.environ.get("SIDE_MODULE_CXXFLAGS", "")
if side_module_cxx_flags.strip():
print("SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').")
print("Maybe it's time to stop overriding them in the script?")
os.environ["SIDE_MODULE_CXXFLAGS"] = os.environ["SIDE_MODULE_CFLAGS"]
# Set defaults for missing empty environment variables
USE_OCCT = os.environ.get("USE_OCCT", "true").lower() == "true"
@@ -382,13 +365,11 @@ if MAC_CROSS_COMPILE_INTEL:
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = ["architecture=x86"]
MAC_CROSS_COMPILE_INTEL_CXX = "clang++ -arch x86_64"
MAC_CROSS_COMPILE_INTEL_CC = "clang -arch x86_64"
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = ["--host=x86_64-apple-darwin"]
else:
MAC_CROSS_COMPILE_INTEL_ARGS = []
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = []
MAC_CROSS_COMPILE_INTEL_CXX = ""
MAC_CROSS_COMPILE_INTEL_CC = ""
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
OFF_ON = ["OFF", "ON"]
BUILD_STATIC = "shared" not in flags
@@ -440,7 +421,6 @@ required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make,
if "wasm" in flags:
# Skip swig build for WASM.
required_commands.append("swig")
required_commands.append("pyodide")
required_commands.remove(yacc)
for cmd in required_commands:
@@ -582,27 +562,16 @@ def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None,
if "wasm" in flags:
wasm.append("emcmake")
cmake_flags: list[str] = []
if not WASM or not WASM_CMAKE_IS_USING_INIT_VARS:
# For WASM we provide flags using just environment variables.
# If we provide them using cmake vars, it will override emscripten toolchain flags.
# Unsure if we need this in general even for non-WASM builds.
cmake_flags.extend(
[
f"-DCMAKE_CXX_FLAGS='{os.environ['CXXFLAGS']}'",
f"-DCMAKE_C_FLAGS='{os.environ['CFLAGS']}'",
]
)
run(
[
*wasm,
"cmake",
P,
*cmake_flags,
*cmake_args,
f"-DCMAKE_BUILD_TYPE={BUILD_CFG}",
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
f"-DCMAKE_CXX_FLAGS='{os.environ['CXXFLAGS']}'",
f"-DCMAKE_C_FLAGS='{os.environ['CFLAGS']}'",
f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}",
],
cwd=cwd,
@@ -941,9 +910,11 @@ 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=f"swig-{SWIG_VERSION}",
name="swig",
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",
@@ -1195,9 +1166,7 @@ if "cgal" in targets:
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}",
@@ -1376,7 +1345,6 @@ 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}")
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}")
@@ -1429,9 +1397,6 @@ if "rocksdb" in 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...")
@@ -1447,14 +1412,20 @@ if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServe
logger.info("\rBuilding executables... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=executables_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], 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,-undefined,dynamic_lookup"
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
if "wasm" in flags:
ADDITIONAL_ARGS = "-Wl,-undefined,suppress"
# Override CMAKE_TOOLCHAIN_FILE because by default emscripten doesn't support building shared binaries.
if WASM_DEBUG:
os.environ["CMAKE_TOOLCHAIN_FILE"] = os.environ["WASM_TOOLCHAIN_FILE"]
# 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)
@@ -1484,6 +1455,9 @@ if "IfcOpenShell-Python" in targets:
if os.path.exists(cache_path):
os.remove(cache_path)
prefix_paths: list[str] = []
if "swig" in targets:
prefix_paths.append(f"{DEPS_DIR}/install/swig")
if python_path:
# We couldn't just prefix PATH and have to provide all variables explicitly,
# see ifcwrap/cmake for the details.
@@ -1500,7 +1474,7 @@ if "IfcOpenShell-Python" in targets:
run_cmake(
"",
cmake_args
+ get_cmake_args_prefix_path()
+ get_cmake_args_prefix_path(prefix_paths)
+ [
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
@@ -1516,7 +1490,7 @@ if "IfcOpenShell-Python" in targets:
logger.info(f"\rBuilding python {python_version} wrapper... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper"], cwd=python_dir)
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
if python_executable:
@@ -1542,10 +1516,7 @@ if "IfcOpenShell-Python" in targets:
return module_dir
if "wasm" in flags:
compile_python_wrapper(
run(["pyodide", "config", "get", "python_version"]),
run(["pyodide", "config", "get", "python_include_dir"]),
)
compile_python_wrapper(os.environ["PYVERSION"], os.environ["PYTHONINCLUDE"])
# Copy setup.py where pyodide build system expects it.
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
+1
View File
@@ -26,6 +26,7 @@ since it's pure cmake without any additional moving parts.
- setup pyodide environment, see above
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
- setup debug build environment using `source pyodide/debug_build_env.sh /path/to/pyodide_root`
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
- it will produce Python package in `IfcOpenShell/ifcopenshell`
- run `pyodide build`
+1 -1
View File
@@ -41,7 +41,7 @@ def pack_dependencies(install_dir: Path) -> None:
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"]] = (
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"):
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 <pyodide_root>"
exit 1
fi
export PYODIDE_ROOT=$(readlink -f "$1")
export PATH="$PYODIDE_ROOT/emsdk/emsdk:$PYODIDE_ROOT/emsdk/emsdk/node/22.16.0_64bit/bin:$PYODIDE_ROOT/emsdk/emsdk/upstream/emscripten:$PATH"
# build-all.py vars.
export WASM_PYTHON_PATH="$PYODIDE_ROOT/cpython/installs/python-3.13.2"
export WASM_TOOLCHAIN_FILE="$PYODIDE_ROOT/pyodide-build/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake"
+3 -2
View File
@@ -5,7 +5,8 @@ import os
from pathlib import Path
import tomllib
from setuptools import Extension, find_packages, setup
from setuptools import Extension, setup
REPO_FOLDER = Path(__file__).parent
@@ -36,7 +37,7 @@ setup(
author_email="thomas@aecgeeks.com",
url="https://ifcopenshell.org",
install_requires=get_dependencies(),
packages=find_packages(include=["ifcopenshell", "ifcopenshell.*"]),
packages=["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"],
-27
View File
@@ -1,27 +0,0 @@
from pathlib import Path
WHEEL_FILENAME = next(
p.name for p in (Path.cwd() / "pyodide").iterdir() if p.name.startswith("ifcopenshell-") and p.suffix == ".whl"
)
def test_ifcopenshell_import(selenium):
selenium.load_package("micropip")
# Important to test it with `micropip.install`
# without any dependencies loaded to ensure micropip will load them automatically.
selenium.run_async(
f"""
import micropip
await micropip.install(f"./{WHEEL_FILENAME}")
import ifcopenshell
ifc_file = ifcopenshell.file()
wall = ifc_file.create_entity("IfcWall")
wall1 = ifc_file.by_type("IfcWall")[0]
print(wall, wall1)
assert wall == wall1, "Wall entity doesn't match"
wall.Name = "Test"
assert wall.Name == "Test", f"Entity name wasn't changed: {{wall}}"
print(wall)
"""
)
+4 -12
View File
@@ -9,8 +9,10 @@ extend-exclude = '''
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|src/ifc2ca/templates/*
|src/ifcconvert/cityjson/*
|src/svgfill
|src/exterior-shell-extractor
|src/pyodide
'''
[tool.pyright]
@@ -19,11 +21,12 @@ disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
# Define here general ruff settings,
# then they will be inherited by projects' .toml files.
# then they will be inherited projects .toml files.
# This allows using assuming different Python version for different projects.
[tool.ruff]
exclude = [
# Submodules.
"src/ifcconvert/cityjson",
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
@@ -59,14 +62,3 @@ 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"]
+1 -4
View File
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=e8eb5e4
OLD:=6924012
.PHONY: bump
bump:
ifndef NEW
@@ -155,9 +155,6 @@ endif
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download six --dest=./wheels
# Required by drawing module
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
# Required by drawing module for markdown hyperlinks in annotations
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download markdown-it-py --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download mdurl --dest=./wheels
# Required by qto and drawing module
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
# Required by the BIM type manager thumbnail generator
@@ -7,6 +7,12 @@
1.0,
1.0
],
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
@@ -48,6 +54,11 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -242,6 +253,12 @@
1.0,
1.0
],
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
@@ -283,6 +300,11 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -355,7 +377,7 @@
1.0,
1.0
],
"scene.display.shading.studio_light": "forest.exr",
"scene.display.shading.studio_light": "Default",
"scene.display.shading.studiolight_background_alpha": 0.0,
"scene.display.shading.studiolight_background_blur": 0.0,
"scene.display.shading.studiolight_intensity": 0.0,
@@ -481,12 +503,18 @@
"scene.eevee.shadow_ray_count": 1,
"scene.eevee.shadow_step_count": 6,
"scene.eevee.shadow_resolution_scale": 1.0,
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
"scene.render.bake_samples": 256,
"scene.render.bake_type": "NORMALS",
"scene.render.bake_user_scale": 0.0,
"scene.render.border_max_x": 1.0,
"scene.render.border_max_y": 1.0,
"scene.render.border_min_x": 0.0,
"scene.render.border_min_y": 0.0,
"scene.render.dither_intensity": 1.0,
"scene.render.engine": "BLENDER_EEVEE",
"scene.render.engine": "BLENDER_EEVEE_NEXT",
"scene.render.film_transparent": false,
"scene.render.filter_size": 1.5,
"scene.render.fps": 24,
@@ -522,6 +550,11 @@
"scene.render.stamp_note_text": "",
"scene.render.threads": 20,
"scene.render.threads_mode": "AUTO",
"scene.render.use_bake_clear": true,
"scene.render.use_bake_lores_mesh": false,
"scene.render.use_bake_multires": false,
"scene.render.use_bake_selected_to_active": false,
"scene.render.use_bake_user_scale": false,
"scene.render.use_border": false,
"scene.render.use_compositing": true,
"scene.render.use_crop_to_border": false,
@@ -594,7 +627,7 @@
0.800000011920929,
0.800000011920929
],
"scene.display.shading.studio_light": "forest.exr",
"scene.display.shading.studio_light": "Default",
"scene.display.shading.studiolight_background_alpha": 0.0,
"scene.display.shading.studiolight_background_blur": 0.0,
"scene.display.shading.studiolight_intensity": 0.0,
-1
View File
@@ -25,7 +25,6 @@ import zipfile
import tempfile
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import bonsai.tool as tool
+2 -2
View File
@@ -28,7 +28,6 @@ import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.unit
@@ -1238,7 +1237,8 @@ class IfcImporter:
if not pset:
return
if "Aggregate_Index" not in pset.keys():
ifcopenshell.api.pset.edit_pset(
ifcopenshell.api.run(
"pset.edit_pset",
self.file,
pset=self.file.by_id(pset["id"]),
properties={"Aggregate_Index": aggregate_index, "Name": name},
@@ -21,7 +21,6 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.group
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
@@ -322,21 +321,6 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
# copy selection query to clipboard
result = ""
for aggregate in all_parts:
aggregate_class = aggregate.is_a()
aggregate_name = aggregate.Name or ""
query = f'parent = "{aggregate_name}"'
if not result:
result = query
else:
result += f" + {query}"
if result:
bpy.context.window_manager.clipboard = result
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
self.one_level_deep = False # <-- forcibly reset
return {"FINISHED"}
+2 -2
View File
@@ -235,8 +235,8 @@ class DecoratorData:
cut_cache = {}
slice_cache = {}
fill_cache = {}
camera_location_checksum = ""
camera_rotation_checksum = ""
camera_location_checksum = None
camera_rotation_checksum = None
@classmethod
def clear_cache(cls):
@@ -19,7 +19,6 @@
import gpu
import bpy
import blf
import os
import math
import bmesh
import shapely
@@ -42,7 +41,7 @@ from bonsai.bim.module.drawing.helper import format_distance
from timeit import default_timer as timer
from functools import cache
from typing import Optional, Union
from collections.abc import Generator, Iterator
from collections.abc import Iterator
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
@@ -173,11 +172,15 @@ class BaseDecorator:
def get_camera_width_mm(self):
# Horrific prototype code to ensure bgl draws at drawing scales
# https://blender.stackexchange.com/questions/16493/is-there-a-way-to-fit-the-viewport-to-the-current-field-of-view
def is_landscape(render):
return render.resolution_x > render.resolution_y
camera = bpy.context.scene.camera
render = bpy.context.scene.render
# Always use the camera's ortho_scale as the width in model space
camera_width_model = camera.data.ortho_scale
if is_landscape(render):
camera_width_model = camera.data.ortho_scale
else:
camera_width_model = camera.data.ortho_scale / render.resolution_y * render.resolution_x
scale = tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
camera_width_mm = scale * camera_width_model
@@ -186,26 +189,29 @@ class BaseDecorator:
def camera_zoom_to_factor(self, zoom):
return math.pow(((zoom / 50) + math.sqrt(2)) / 2, 2)
def get_splines(self, obj: bpy.types.Object) -> Generator[list[Vector]]:
def get_splines(self, obj):
"""Iterates through splines
Args:
obj: Blender object with Curve data
:param obj: Blender object with Curve data
:yield: points of each spline, world coords
Yields:
verts: points of each spline, world coords
"""
assert type(obj.data) is bpy.types.Curve
for spline in obj.data.splines:
spline_points = spline.bezier_points if spline.bezier_points else spline.points
if len(spline_points) < 2:
continue
yield [obj.matrix_world @ p.co for p in spline_points]
def get_path_geom(self, obj: bpy.types.Object, topo: bool = True):
def get_path_geom(self, obj, topo=True):
"""Parses path geometry into line segments
:param obj: Blender object with data of type Curve
:param topo: if types of vertices are needed
Args:
obj: Blender object with data of type Curve
topo: bool; if types of vertices are needed
:return: vertices: 3-tuples of coords
Returns:
vertices: 3-tuples of coords
indices: 2-tuples of each segment verices' indices
topology: types of vertices
0: internal
@@ -236,9 +242,11 @@ class BaseDecorator:
def get_mesh_geom(self, obj, check_mode=True):
"""Parses mesh geometry into line segments
:param obj: Blender object with data of type Mesh
Args:
obj: Blender object with data of type Mesh
:return: vertices: 3-tuples of coords
Returns:
vertices: 3-tuples of coords
indices: 2-tuples of each segment verices' indices
"""
if check_mode and obj.data.is_editmode:
@@ -369,9 +377,11 @@ class BaseDecorator:
):
"""Draw text label
:param pos: bottom-center
:param multiline: ``\n`` characters will be interpreted as line breaks
aligned and centered at segment middle
Args:
pos: bottom-center
multiline: \n characters will be interpreted as line breaks
aligned and centered at segment middle
NOTE: `blf.draw` seems to ignore the \n character, so we have to split the text ourselves
and use the `line_no` argument of `draw_label`
@@ -1631,13 +1641,6 @@ class CutDecorator:
if not context.scene.camera:
return
# Check if any viewport is in local view - skip decorations if so
for area in context.screen.areas:
if area.type == "VIEW_3D":
space = area.spaces.active
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
return # Don't draw decorations in local view
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
self.fallback_colour = (0.3, 0.3, 0.3, 1)
@@ -1728,23 +1731,20 @@ class CutDecorator:
def cache_camera_matrix(self):
obj = bpy.context.scene.camera
# Explicit `dtype` for Blender <5.0 compatibility.
DecoratorData.camera_location_checksum = repr(
np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()
)
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
def is_camera_moved(self):
if not DecoratorData.camera_location_checksum:
self.cache_camera_matrix()
return True # Let's be conservative
obj = bpy.context.scene.camera
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum), dtype=np.float32)
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
loc_real = np.array(obj.matrix_world.translation).flatten()
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
self.cache_camera_matrix()
return True
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum), dtype=np.float32).reshape(3, 3)
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3())
rot_dot = np.dot(rot_check, rot_real.T)
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
@@ -1974,51 +1974,15 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
prefs = tool.Blender.get_addon_preferences()
if drawing_font := prefs.doc.drawing_font:
# Try Blender addon font folder
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
if not drawing_font_path.is_file():
# 2 — Fallback search: Windows font directories
# TODO - Linux
win_font_dirs = [
Path(os.environ.get("WINDIR", "C:\\Windows")) / "Fonts",
# Add any custom enterprise paths here if needed
]
found_font = None
for font_dir in win_font_dirs:
candidate = font_dir / drawing_font
if candidate.is_file():
found_font = candidate
break
if found_font:
drawing_font_path = found_font
else:
print(f"[BIM] Font '{drawing_font}' not found in addon or Windows fonts.")
return # Bail out without assigning fonts
font_id = blf.load(str(drawing_font_path))
for decorator in self.decorators.values():
decorator.font_id = font_id
if drawing_font_path.is_file():
font_id = blf.load(drawing_font_path.__str__())
for decorator in self.decorators.values():
decorator.font_id = font_id
def __call__(self, context):
# Check if any viewport is in local view - skip decorations if so
for area in context.screen.areas:
if area.type == "VIEW_3D":
space = area.spaces.active
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
return # Don't draw decorations in local view
# disable decorations when not in camera view
if context.region_data.view_perspective != "CAMERA":
return
if not DrawingsData.is_loaded:
DrawingsData.load()
+12 -11
View File
@@ -18,7 +18,6 @@
import bpy
import math
import ifcopenshell.util.element
import mathutils.geometry
import ifcopenshell
import ifcopenshell.util.unit
@@ -422,8 +421,8 @@ def ortho_view_frame(
Similar to `bpy.types.Camera.view_frame`
:param camera: camera of drawing
:param margin: margins, in scene units
:arg camera: camera of drawing
:arg margin: margins, in scene units
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
props = tool.Drawing.get_camera_props(camera)
@@ -444,8 +443,8 @@ def almost_zero(v):
def clip_segment(bounds, segm):
"""Clipping line segment to bounds
:param bounds: (xmin, xmax, ymin, ymax)
:param segm: 2 vertices of the segment
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
# LiangBarsky algorithm
@@ -494,8 +493,8 @@ def clip_segment(bounds, segm):
def elevate_segment(bounds, segm):
"""Elevate line xy-perpendicular segment vertically
:param bounds: (xmin, xmax, ymin, ymax)
:param segm: 2 vertices of the segment
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
_, _, ymin, ymax, zmin, _ = bounds
@@ -545,11 +544,13 @@ 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.
: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.
Args:
obj: The Blender object representing the element.
element: The IFC entity for the object.
abs_z: The absolute Z value in world coordinates.
:return: Relative Z value if the element is inside a spatial container,
Returns:
Relative Z value if the element is inside a spatial container,
otherwise the absolute Z.
"""
z = abs_z
@@ -932,31 +932,6 @@ 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)
@@ -1312,6 +1287,11 @@ 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:
@@ -2721,59 +2701,21 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def set_raster_style(self, context: bpy.types.Context) -> None:
scene = context.scene # Do not remove. It is used in exec later
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used in exec later
style: dict[str, Any] = json.loads(self.drawing_style.raster_style)
VIEWPORT_SHADING_TYPE = "scene.display.shading.type"
style = json.loads(self.drawing_style.raster_style)
def preprocess(path: str, value: Any) -> tuple[str, Any, bool, bool]:
warning = False
skip = False
BLENDER_5_REMOVED = (
"scene.render.bake_bias",
"scene.render.bake_margin",
"scene.render.bake_margin_type",
"scene.render.bake_samples",
"scene.render.bake_type",
"scene.render.bake_user_scale",
"scene.render.use_bake_clear",
"scene.render.use_bake_lores_mesh",
"scene.render.use_bake_multires",
"scene.render.use_bake_selected_to_active",
"scene.render.use_bake_user_scale",
)
# 25.11.07, Blender 5+
if path == "scene.render.engine" and value in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
if value == "BLENDER_EEVEE_NEXT":
print(
f"Warning: Value 'BLENDER_EEVEE_NEXT' is outdated for property '{path}' "
"since Blender 5.0 and should be replaced with 'BLENDER_EEVEE' in shading_styles.json."
)
warning = True
value = tool.Blender.get_eevee_name()
elif tool.Blender.BLENDER_5 and path in BLENDER_5_REMOVED:
# @25.05.12
if path == "scene.render.engine" and value == "BLENDER_EEVEE":
value = "BLENDER_EEVEE_NEXT"
print(
f"Warning: Property '{path}' is removed "
"since Blender 5.0 and should be also removed from shading_styles.json."
)
warning = True
skip = True
# @25.11.11
elif (
path == "scene.display.shading.studio_light"
and value == "Default"
and style[VIEWPORT_SHADING_TYPE] in ("RENDERED", "MATERIAL")
):
value = "forest.exr"
print(
f"Warning: Value 'Default' for property '{path}' and "
f"'{VIEWPORT_SHADING_TYPE}' = '{style[VIEWPORT_SHADING_TYPE]}' is outdated "
"and should be replaced with 'forest.exr' in shading_styles.json."
f"Warning: Value 'BLENDER_EEVEE' is outdated for property '{path}' "
"since Blender 4.2 and should be replaced with 'BLENDER_EEVEE_NEXT' in shading_styles.json."
)
warning = True
# @25.05.12
elif path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
if path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
value = "THEME"
print(
f"Warning: Value 'MATERIAL' is outdated for property '{path}' "
@@ -2807,16 +2749,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
return path, value, warning, skip
paths = list(style.keys())
PRIORITY_PATHS = (
# `scene.display.shading.studio_light` values depend on `.type`
# so we got to set it first.
VIEWPORT_SHADING_TYPE,
)
paths.sort(key=lambda p: p not in PRIORITY_PATHS)
for path in paths:
value = style[path]
for path, value in style.items():
path, value, warning, skip = preprocess(path, value)
self.has_warnings_during_activation |= warning
@@ -3481,15 +3414,6 @@ class LoadDrawings(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Operator is not accessible through UI if IFC project is not saved,
# but adding poll check to avoid using Drawings UI in scripts with unsaved project.
if tool.Ifc.get_path():
return True
cls.poll_message_set("IFC project is not saved.")
return False
def _execute(self, context):
core.load_drawings(tool.Drawing)
@@ -544,10 +544,11 @@ class Scheduler:
"""
Adds text to svg.
:param p_tags: list of cell's P tags from odt file
:param box_alignment: alignment of text in box
:param wrap_text: if True, text will be wrapped to fit in cell
:param cell_width: width of cell, used for wrapping text
Args:
p_tags: list of cell's P tags from odt file
box_alignment: alignment of text in box
wrap_text: if True, text will be wrapped to fit in cell
cell_width: width of cell, used for wrapping text
"""
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
@@ -341,29 +341,17 @@ class SheetBuilder:
assert style_data is not None
text = ""
brackets_level = 0
selector_buffer = "" # Buffer to accumulate selectors across lines
for l in style_data:
if l == "{":
if brackets_level == 0:
# Get all accumulated selector text (may span multiple lines)
# Find where the last rule ended (after last }) or start of text
last_close = text.rfind("}")
if last_close == -1:
selector_text = text
text = ""
else:
selector_text = text[last_close + 1 :]
text = text[: last_close + 1]
# Process all selectors (split by comma)
cur_line = text.splitlines()[-1]
text = text[: -len(cur_line)]
css_selectors = []
for css_selector in selector_text.split(","):
css_selector = css_selector.strip()
if css_selector: # Only process non-empty selectors
css_selector = f"{css_selector}.{prefix}"
css_selectors.append(css_selector)
# making sure cases like "text, tspan" will be
# converted to "text.prefix, tspan.prefix"
for css_selector in cur_line.split(","):
css_selector = f"{css_selector.strip()}.{prefix}"
css_selectors.append(css_selector)
text += ", ".join(css_selectors) + " "
brackets_level += 1
elif l == "}":
@@ -202,15 +202,6 @@ class SvgWriter:
) -> Self:
self.precision = precision
self.decimal_places = decimal_places
# Sort annotations to ensure FILLAREA types are drawn first (at the bottom)
def sort_key(element):
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
# Return 0 for FILLAREA to draw them first, 1 for everything else
return 0 if predefined_type == "FILL_AREA" else 1
annotations = sorted(annotations, key=sort_key)
for element in annotations:
obj = tool.Ifc.get_object(element)
if (
+1 -1
View File
@@ -116,7 +116,7 @@ class BIM_PT_camera(Panel):
# See #6686.
if (
props.has_underlay
and str(render.engine) == tool.Blender.get_eevee_name()
and str(render.engine) == "BLENDER_EEVEE_NEXT"
and ((megapixels := (render.resolution_x * render.resolution_y / 10**6)) > MEGAPIXELS_WARNING_THRESHOLD)
):
box = self.layout.box()
@@ -30,7 +30,6 @@ classes = (
operator.AddSweptAreaSolidItem,
operator.AssignRepresentationLayer,
operator.CopyRepresentation,
operator.DirectProfileEdit,
operator.DisableEditingRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemStyle,
operator.DisableEditingRepresentationItems,
@@ -161,8 +160,6 @@ def register():
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
addon_keymaps.append((km, kmi))
# Deletion.
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
addon_keymaps.append((km, kmi))
@@ -181,8 +178,6 @@ def register():
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
kmi.properties.should_save = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
addon_keymaps.append((km, kmi))
+73 -390
View File
@@ -42,7 +42,6 @@ import bonsai.core.geometry
import bonsai.core.geometry as core
import bonsai.core.aggregate
import bonsai.core.nest
import bonsai.core.spatial
import bonsai.core.style
import bonsai.core.root
import bonsai.core.drawing
@@ -810,17 +809,9 @@ class OverrideDelete(bpy.types.Operator):
@classmethod
def poll(cls, context):
# Match `object.delete` poll for consistency.
# `object.delete` poll just checks for OBJECT mode.
poll = bpy.ops.object.delete.poll()
if poll:
return True
cls.poll_message_set("Only available in OBJECT mode")
return False
return len(context.selected_objects) > 0
def execute(self, context):
if not context.selected_objects:
return {"FINISHED"}
# Deep magick from the dawn of time
if tool.Ifc.get() is None:
bpy.ops.object.delete(use_global=self.use_global, confirm=self.confirm)
@@ -832,8 +823,6 @@ class OverrideDelete(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def invoke(self, context, event):
if not context.selected_objects:
return {"FINISHED"}
assert context.window_manager
ifc_file = tool.Ifc.get()
if ifc_file is None:
@@ -1041,18 +1030,9 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context) -> bool:
# Match `outliner.delete` poll for consistency.
# `outliner.delete` just checks `area.type` == `OUTLINER`.
poll = bpy.ops.outliner.delete.poll()
if poll:
return True
cls.poll_message_set("Only available from Outliner.")
return False
return len(getattr(context, "selected_ids", [])) > 0
def execute(self, context):
if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"}
# In this override, we don't check self.hierarchy. This effectively
# makes Delete and Delete Hierarchy identical. This is on purpose, since
# non-hierarchical deletion may imply a whole bunch of potentially
@@ -1071,9 +1051,6 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def invoke(self, context, event):
if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"}
assert context.window_manager
ifc_file = tool.Ifc.get()
if ifc_file:
@@ -1144,18 +1121,10 @@ class OverrideDuplicateMove(bpy.types.Operator):
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@classmethod
def poll(cls, context) -> bool:
# Match `object.duplicate_move` poll for consistency.
# `object.duplicate_move` poll checks for OBJECT mode.
poll = bpy.ops.object.duplicate_move.poll()
if poll:
return True
cls.poll_message_set("Only available in OBJECT mode")
return False
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
if not context.selected_objects:
return {"FINISHED"}
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
@@ -1178,7 +1147,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
@staticmethod
def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False):
objects_to_remove: set[bpy.types.Object] = set()
objects_to_remove = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -1236,9 +1205,6 @@ class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_options = {"REGISTER", "UNDO"}
OldToNewType = dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.object_duplicate_move_linked_aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
@@ -1261,9 +1227,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
all_old_to_new = {} # Track all duplicates created
old_to_new = {}
def select_objects_and_add_data(element: ifcopenshell.entity_instance) -> None:
def select_objects_and_add_data(element):
add_linked_aggregate_group(element)
obj = tool.Ifc.get_object(element)
obj.select_set(True)
@@ -1277,6 +1243,8 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
select_objects_and_add_data(part)
else:
index = add_linked_aggregate_pset(part, index)
# index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
@@ -1289,15 +1257,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name=self.pset_name)
ifcopenshell.api.pset.edit_pset(
ifc_file,
pset=pset,
properties=properties,
)
index += 1
else:
pass
return index
def add_linked_aggregate_group(element: ifcopenshell.entity_instance) -> None:
def add_linked_aggregate_group(element):
linked_aggregate_group = None
product_groups_name = [
r.RelatingGroup.Name
@@ -1310,21 +1283,25 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
linked_aggregate_group = ifcopenshell.api.group.add_group(ifc_file, name=self.group_name)
ifcopenshell.api.group.assign_group(ifc_file, products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new: OldToNewType) -> None:
def custom_incremental_naming_for_element_assembly(old_to_new):
for new in old_to_new.values():
if new[0].is_a("IfcElementAssembly"):
group_elements: list[ifcopenshell.entity_instance] = next(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
)
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
new_obj = tool.Ifc.get_object(new[0])
new_obj.name = f"{pset['Name']}_{pset['Aggregate_Index']:02d}"
new_obj.name = pset["Name"] + "_" + str(pset["Aggregate_Index"])
def get_max_index(parts):
psets = [
ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
for p in parts
if ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
]
if psets:
index = max([i["Index"] for i in psets if i])
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
@@ -1338,28 +1315,23 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
if linked_aggregate_group:
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
group_elements: list[ifcopenshell.entity_instance] = next(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
)
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.pset.add_pset(ifc_file, product=new[0], name=self.pset_name)
if pset["Index"] == 0:
group_elements = []
if new[0].is_a("IfcElementAssembly"):
group_elements = next(
(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
and "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
),
[],
)
properties = {
"Index": pset["Index"],
"Name": pset["Name"],
"Aggregate_Index": len(group_elements) - 1 if group_elements else 0,
"Aggregate_Index": len(group_elements) - 1,
}
else:
properties = {"Index": pset["Index"]}
@@ -1379,73 +1351,43 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
location_diff = new_obj.location - base_obj_location
new_obj.location = context.scene.cursor.location + location_diff
# Process multiple selected aggregates
selected_aggregates = []
for obj in context.selected_objects:
selected_element = tool.Ifc.get_entity(obj)
if not selected_element:
continue
# Find the aggregate element
if selected_element.is_a("IfcElementAssembly"):
selected_aggregates.append(selected_element)
elif selected_element.Decomposes:
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
selected_aggregates.append(selected_element.Decomposes[0].RelatingObject)
# Remove duplicates
selected_aggregates = list(set(selected_aggregates))
if not selected_aggregates:
self.report({"INFO"}, "No Linked Aggregates selected.")
if len(context.selected_objects) != 1:
return {"FINISHED"}
# Deselect all first
selected_obj = context.selected_objects[0]
selected_element = tool.Ifc.get_entity(selected_obj)
assert selected_element
if selected_element.is_a("IfcElementAssembly"):
pass
elif selected_element.Decomposes:
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
selected_element = selected_element.Decomposes[0].RelatingObject
selected_obj = tool.Ifc.get_object(selected_element)
else:
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
return {"FINISHED"}
select_objects_and_add_data(selected_element)
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, selected_element)
bpy.ops.object.select_all(action="DESELECT")
# Process each selected aggregate
for aggregate in selected_aggregates:
aggregate_obj = tool.Ifc.get_object(aggregate)
# Select and prepare the aggregate for duplication
select_objects_and_add_data(aggregate)
# Duplicate the aggregate
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
all_old_to_new.update(old_to_new) # Collect all duplicates
# Recreate aggregate structure
tool.Root.recreate_aggregate(old_to_new)
# Copy linked aggregate data
copy_linked_aggregate_data(old_to_new)
# Apply custom naming
custom_incremental_naming_for_element_assembly(old_to_new)
# Apply 3D cursor location if requested
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, aggregate)
# Deselect for next iteration
bpy.ops.object.select_all(action="DESELECT")
# Select all newly created aggregates
for aggregate in selected_aggregates:
if aggregate in all_old_to_new:
new_aggregate = all_old_to_new[aggregate][0]
new_aggregate_obj = tool.Ifc.get_object(new_aggregate)
if new_aggregate_obj:
new_aggregate_obj.select_set(True)
# Set active object to the first new aggregate
if all_old_to_new:
first_new_aggregate = next(iter(all_old_to_new.values()))[0]
context.view_layer.objects.active = tool.Ifc.get_object(first_new_aggregate)
new_aggregate = old_to_new[selected_element][0]
tool.Blender.set_active_object(tool.Ifc.get_object(new_aggregate))
bonsai.bim.handler.refresh_ui_data()
return all_old_to_new
return old_to_new
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
@@ -1481,7 +1423,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
self.pset_name = "BBIM_Linked_Aggregate"
refresh_start_time = time()
old_to_new = {}
original_data: dict[int, dict[int, dict[str, Any]]] = {}
original_data: dict[int, dict[int, str]] = {}
def delete_objects(element: ifcopenshell.entity_instance) -> None:
"""Remove IfcElementAssembly and it's parts."""
@@ -1527,8 +1469,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
)
tool.Blender.update_viewport()
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, dict[str, Any]]]:
group: int = next(
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]:
group = next(
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
@@ -1536,8 +1478,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
).id()
original_data[group] = {}
pset: dict[str, Any] = ifcopenshell.util.element.get_pset(element, self.pset_name)
index: int = pset["Index"]
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
annotations = get_assignments(element)
container = ifcopenshell.util.element.get_container(element)
original_data[group][index] = {
@@ -1551,7 +1493,6 @@ 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:
@@ -2545,264 +2486,6 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
props.is_changing_mode = False
class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.direct_profile_edit"
bl_label = "IFC Direct Profile Edit"
bl_description = "Directly enter profile/axis edit mode, or exit back to object mode"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Only allow in OBJECT or EDIT modes
if context.mode not in ("OBJECT", "EDIT_MESH", "EDIT_CURVE"):
return False
# Must have an active object
if not context.active_object:
return False
# In edit mode, we're good to go (we'll exit)
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
return True
# In object mode, check if it's an editable object type
obj = context.active_object
# Only mesh and curve objects are editable
if obj.type not in ("MESH", "CURVE"):
return False
return True
def _execute(self, context):
# Ensure we're only working with the active object
# Deselect all other objects if multiple are selected
active_obj = context.active_object
if active_obj and context.mode == "OBJECT":
if len(context.selected_objects) > 1:
self.report(
{"WARNING"},
"Currently Direct Profile Edit only works on one object at a time. Operating on active object only.",
)
bpy.ops.object.select_all(action="DESELECT")
active_obj.select_set(True)
context.view_layer.objects.active = active_obj
# If we're already in edit mode, exit to object mode and SAVE changes
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
return self.handle_exit_edit_mode(context)
# We're in object mode, try to enter profile/axis edit
return self.handle_enter_edit_mode(context)
def handle_exit_edit_mode(self, context):
"""Handle exiting from edit mode and saving changes."""
obj = context.active_object
if not obj:
return {"CANCELLED"}
# Check if we're editing a representation item
if tool.Geometry.is_representation_item(obj):
return self.exit_item_edit_mode(context, obj)
# Check if we're editing an element-level profile or axis
element = tool.Ifc.get_entity(obj)
if element and tool.Geometry.has_mesh_properties(obj.data):
return self.exit_element_edit_mode(context, obj, element)
# For other edit modes, use standard operator
return bpy.ops.bim.override_mode_set_object()
def exit_item_edit_mode(self, context, obj):
"""Exit from SweptAreaSolid item editing."""
try:
item = tool.Geometry.get_active_representation(obj)
if not item or not item.is_a("IfcSweptAreaSolid"):
return bpy.ops.bim.override_mode_set_object()
# Use the standard save workflow for items
from bonsai.bim.module.model.decorator import ProfileDecorator
ProfileDecorator.uninstall()
tool.Blender.toggle_edit_mode(context)
profile = tool.Model.export_profile(obj)
if not profile:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
tool.Blender.toggle_edit_mode(context)
ProfileDecorator.install(context)
return {"CANCELLED"}
old_profile = item.SweptArea
profile.ProfileName = old_profile.ProfileName
ifc_file = tool.Ifc.get()
for inverse in ifc_file.get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
ifcopenshell.util.element.remove_deep2(ifc_file, old_profile)
props = tool.Geometry.get_geometry_props()
if props.representation_obj:
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.disable_item_mode()
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to save item changes: {str(e)}")
return {"CANCELLED"}
def exit_element_edit_mode(self, context, obj, element):
"""Exit from element-level profile or axis editing."""
try:
mesh_props = tool.Geometry.get_mesh_props(obj.data)
# Check if we're editing axis
if mesh_props.subshape_type == "AXIS":
return bpy.ops.bim.edit_extrusion_axis()
# Otherwise, we're editing a profile
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion:
return bpy.ops.bim.edit_extrusion_profile()
except Exception as e:
self.report({"ERROR"}, f"Failed to save profile/axis changes: {str(e)}")
return {"CANCELLED"}
# Fallback to standard operator
return bpy.ops.bim.override_mode_set_object()
def handle_enter_edit_mode(self, context):
"""Handle entering edit mode from object mode."""
obj = context.active_object
if not obj:
return {"CANCELLED"}
# Check if object has mesh or curve data
if not hasattr(obj, "data") or not obj.data:
self.report({"INFO"}, "Object has no editable data")
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
self.report({"INFO"}, "Active object is not an IFC element")
return {"CANCELLED"}
# Check if this is a LAYER2 element (wall, railing, etc.)
try:
material_usage = tool.Model.get_usage_type(element)
except:
material_usage = None
if material_usage == "LAYER2":
self.report({"ERROR"}, "LAYER2 elements (walls, railings, etc.) cannot use profile editing.")
return {"CANCELLED"}
# Try to get representation
try:
representation = tool.Geometry.get_active_representation(obj)
except:
representation = None
if not representation:
self.report({"INFO"}, "Object has no active representation")
return {"CANCELLED"}
# Check for PROFILE usage (beams, columns, members) - should edit axis
if material_usage == "PROFILE":
try:
return bpy.ops.bim.enable_editing_extrusion_axis()
except Exception as e:
self.report({"ERROR"}, f"Failed to enable axis editing: {str(e)}")
return {"CANCELLED"}
# Check if this is an element with an extrusion profile (LAYER3, etc)
try:
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion and not tool.Geometry.is_representation_item(obj):
return bpy.ops.bim.enable_editing_extrusion_profile()
except Exception as e:
self.report({"ERROR"}, f"Failed to enable profile editing: {str(e)}")
return {"CANCELLED"}
# Otherwise, try to enter item mode and edit SweptAreaSolid
return self.enter_item_edit_mode(context, obj)
def enter_item_edit_mode(self, context, obj):
"""Enter item mode for SweptAreaSolid editing."""
try:
props = tool.Geometry.get_geometry_props()
if not props.representation_obj:
bpy.ops.bim.import_representation_items()
props = tool.Geometry.get_geometry_props()
# Find the SweptAreaSolid item
swept_solid_obj = None
for item_obj_data in props.item_objs:
item_obj = item_obj_data.obj
if not item_obj:
continue
try:
item = tool.Geometry.get_active_representation(item_obj)
if item and item.is_a("IfcSweptAreaSolid"):
swept_solid_obj = item_obj
break
except:
continue
if not swept_solid_obj:
self.report({"INFO"}, "No editable SweptAreaSolid item found")
tool.Geometry.disable_item_mode()
return {"CANCELLED"}
# Select and activate the swept solid object
bpy.ops.object.select_all(action="DESELECT")
swept_solid_obj.select_set(True)
context.view_layer.objects.active = swept_solid_obj
# Import the profile to prepare for editing
item = tool.Geometry.get_active_representation(swept_solid_obj)
if item and item.is_a("IfcSweptAreaSolid"):
profile = item.SweptArea
res = tool.Model.import_profile(profile, obj=swept_solid_obj)
if res is None:
self.report({"ERROR"}, "Couldn't import profile for editing")
tool.Geometry.disable_item_mode()
return {"CANCELLED"}
tool.Ifc.link(item, swept_solid_obj.data)
tool.Blender.toggle_edit_mode(context)
from bonsai.bim.module.model.decorator import ProfileDecorator
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to enter item edit mode: {str(e)}")
try:
tool.Geometry.disable_item_mode()
except:
pass
return {"CANCELLED"}
return {"CANCELLED"}
class FlipObject(bpy.types.Operator):
bl_idname = "bim.flip_object"
bl_label = "Flip Object"
+4 -5
View File
@@ -96,12 +96,11 @@ def update_shadow_mode(self: "BIMSolarProperties", context: bpy.types.Context) -
sun_props = tool.Blender.get_sun_props()
assert sun_props
if sun_props.sun_object is None:
light = bpy.data.lights.new(name="Sun", type="SUN")
sun = bpy.data.objects.new("Sun", light)
context.scene.collection.objects.link(sun)
sun_props.sun_object = sun
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
bpy.ops.object.move_to_collection(collection_index=0)
sun_props.sun_object = bpy.context.active_object
update_sun_path(self)
context.scene.render.engine = tool.Blender.get_eevee_name()
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
assert context.scene.display
assert context.scene.display.shading
context.scene.display.shading.light = "FLAT"
+18 -2
View File
@@ -16,7 +16,6 @@
# 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
@@ -193,7 +192,24 @@ 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=calendar.month_name[props.month])
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, "day")
row = self.layout.row(align=True)
+1 -1
View File
@@ -478,7 +478,7 @@ 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 = "Add Door"
bl_label = "Door"
bl_options = {"REGISTER", "UNDO"}
@classmethod
+11 -131
View File
@@ -348,136 +348,24 @@ class BIMArrayProperties(PropertyGroup):
def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None:
"""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)
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:
# 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)
self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1)
else:
# 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
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
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:
# 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)
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
else:
# 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
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
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
StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"]
class BIMStairProperties(PropertyGroup):
@@ -520,7 +408,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(tool.Model.StairType)],
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(StairType)],
default="CONCRETE",
update=validate_nosing_value,
)
@@ -537,7 +425,6 @@ 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",
@@ -605,18 +492,16 @@ 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:
non_si_units_props += ("custom_first_last_tread_run",)
stair_kwargs["custom_first_last_tread_run"] = (None, None)
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
else:
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
if not convert_to_project_units:
return stair_kwargs
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, non_si_units_props)
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
return stair_kwargs
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
@@ -628,19 +513,14 @@ 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"] = 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
kwargs["custom_tread_lock"] = not any(ct != 0.0 and ct != tread_run for ct in custom_treads)
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
+1 -1
View File
@@ -131,7 +131,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 = "Add Stair"
bl_label = "Stair"
bl_options = {"REGISTER", "UNDO"}
@classmethod
+3 -7
View File
@@ -17,6 +17,7 @@
# 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
@@ -35,12 +36,7 @@ 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, 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
from typing import Any
class BIM_MT_type_manager_menu(bpy.types.Menu):
@@ -60,7 +56,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")
+1 -1
View File
@@ -399,7 +399,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 = "Add Window"
bl_label = "Window"
bl_options = {"REGISTER", "UNDO"}
@classmethod
@@ -1322,20 +1322,6 @@ 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:
+2 -17
View File
@@ -859,23 +859,8 @@ 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)
# 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)
assert isinstance(open_obj, bpy.types.Object)
opening_area += get_net_top_area(open_obj, angle=angle)
else:
continue
+2 -5
View File
@@ -22,7 +22,6 @@ 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):
@@ -149,8 +148,7 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
}
ifc_file = tool.Ifc.get()
with Profiler("Quantify function time:"):
results = ifc5d.qto.quantify(ifc_file, elements, rules)
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
not_quantified_elements = elements - set(results.keys())
@@ -192,8 +190,7 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
) -> set[ifcopenshell.entity_instance]:
rules = ifc5d.qto.rules[rule]
ifc_file = tool.Ifc.get()
with Profiler("Quantify function time:"):
results = ifc5d.qto.quantify(ifc_file, elements, rules)
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,7 +21,6 @@ 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):
@@ -234,7 +233,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 container selected in Spatial Manager.
Copies selected 3D elements in the viewport to the selected spatial containers
Example: bulk copy a wall to multiple storeys
@@ -243,13 +242,9 @@ 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() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
container: int
container: bpy.props.IntProperty()
def _execute(self, context):
objs = tool.Spatial.get_selected_objects_without_containers()
@@ -455,7 +455,7 @@ class ActivateExternalStyle(bpy.types.Operator):
return {"CANCELLED"}
self.copy_material_attributes(db["data_block"], material)
if tool.Style.get_use_nodes(material):
if material.use_nodes:
tool.Blender.copy_node_graph(material, db["data_block"])
bpy.data.materials.remove(db["data_block"])
return {"FINISHED"}
+3 -4
View File
@@ -99,8 +99,7 @@ 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."""
# Named not as `active_system_index` to match `BIMGroupProperties`.
active_group_index: IntProperty(name="Active System Index")
active_system_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")
@@ -114,7 +113,7 @@ class BIMSystemProperties(PropertyGroup):
is_adding: bool
systems: bpy.types.bpy_prop_collection_idprop[System]
expanded_groups_json: str
active_group_index: int
active_system_index: int
active_system_id: int
edited_system_id: int
system_class: str
@@ -122,7 +121,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_group_index)
return tool.Blender.get_active_uilist_element(self.systems, self.active_system_index)
def update_active_zone_index(self: "BIMZoneProperties", context: object) -> None:
+1 -1
View File
@@ -114,7 +114,7 @@ class BIM_PT_systems(Panel):
self.props,
"systems",
self.props,
"active_group_index",
"active_system_index",
)
if self.props.edited_system_id:
+2 -2
View File
@@ -786,7 +786,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
material = bpy.data.materials.get("Section Override")
if not material:
material = bpy.data.materials.new("Section Override")
tool.Style.set_use_nodes(material, True)
material.use_nodes = True
props = tool.Blender.get_bim_props()
if props.should_section_selected_objects:
@@ -809,7 +809,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
def override_materials(self):
override = bpy.data.node_groups.get("Section Override")
for material in bpy.data.materials:
tool.Style.set_use_nodes(material, True)
material.use_nodes = True
if material.node_tree.nodes.get("Section Override"):
continue
# In EEVEE rendering engine, `blend_mode` is deprecated and replaced by `surface_render_method`
+1 -6
View File
@@ -20,12 +20,7 @@ import ifcopenshell
import ifcopenshell.util.pset
import bonsai.tool as tool
import bpy
from typing import TYPE_CHECKING
if TYPE_CHECKING or bpy.app.version >= (5, 0, 0):
import _bpy_restrict_state as bpy_restrict_state
else:
import bpy_restrict_state
import bpy_restrict_state
class IfcSchema:
+10 -48
View File
@@ -324,51 +324,25 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
bl_idname = tool.Blender.get_blender_addon_package_name()
svg2pdf_command: StringProperty(
name="SVG to PDF Command",
description=(
"print sheet to pdf together with svg. Leave blank svg is just created, "
'E.g. [["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "svg", "-o", "pdf"]]'
'\n `"svg"`, `"pdf"` will be replaced automatically.'
),
description='print sheet to pdf together with svg. Leave blank svg is just created, E.g. [["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "svg", "-o", "pdf"]]',
)
svg2dxf_command: StringProperty(
name="SVG to DXF Command",
description=(
'E.g. `[["inkscape", "svg", "-o", "eps"], '
'["pstoedit", "-dt", "-f", "dxf:-polyaslines -mm", "eps", "dxf", "-psarg", "-dNOSAFER"]]`'
'\n `"svg"`, `"eps"`, `"dxf"` will be replaced automatically.'
),
description='E.g. [["inkscape", "svg", "-o", "eps"], ["pstoedit", "-dt", "-f", "dxf:-polyaslines -mm", "eps", "dxf", "-psarg", "-dNOSAFER"]]',
)
svg_command: StringProperty(
name="SVG Command",
description=(
"Software to open generated drawing and sheets. Leave blank system default for .svg is used "
'E.g. [["path to application eg. /Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "path"]]'
'\n `"path"` will be replaced with the path to the generated .svg file.'
),
description='Software to open generated drawing and sheets. Leave blank system default for .svg is used E.g. [["path to application eg. /Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "path"]]',
)
layout_svg_command: StringProperty(
name="Layout SVG Command",
description=(
"Software to open layouts, before generated. Leave blank system default for .svg is used. "
'E.g. `[["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "path"]]`'
'\n `"path"` will be replaced with the path to the generated .svg file.'
),
description='Software to open layouts, before generated. Leave blank system default for .svg is used E.g. [["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "path"]]',
)
pdf_command: StringProperty(
name="PDF Command",
description=(
"Software to open .pdf, leave blank uses system default. "
'E.g. `[["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "path"]]`'
'\n `"path"` will be replaced with the path to the generated .pdf file.'
),
)
spreadsheet_command: StringProperty(
name="Spreadsheet Command",
description=(
'E.g. [["libreoffice", "path"]]'
'\n `"path"` will be replaced with the path to the generated spreadsheet file.'
),
description='Software to open .pdf, leave blank uses system default. E.g. [["path to application eg. /Applications/Inkscape.app/Contents/MacOS/inkscape", "path"]]',
)
spreadsheet_command: StringProperty(name="Spreadsheet Command", description='E.g. [["libreoffice", "path"]]')
should_hide_empty_props: BoolProperty(
name="Hide Empty Properties",
default=True,
@@ -377,18 +351,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_setup_workspace: BoolProperty(
name="Setup Workspace Layout for BIM",
default=True,
description=(
"If enabled, this will add a default workspace dedicated to working with BIM models.\n"
"It is recommended to keep this `Enabled`"
),
description="If enabled, this will add a default workspace dedicated to working with BIM models.\nIt is recommended to keep this `Enabled`",
)
activate_workspace: BoolProperty(
name="Activate BIM Workspace on Startup",
default=True,
description=(
"If enabled, this will automatically activate the BIM workspace when opening a project.\n"
"It is recommended to keep this `Enabled`"
),
description="If enabled, this will automatically activate the BIM workspace when opening a project.\nIt is recommended to keep this `Enabled`",
)
should_setup_toolbar: BoolProperty(
name="Always Show Toolbar In 3D Viewport",
@@ -398,9 +366,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_use_snap: BoolProperty(
name="Enable Snapping on Startup",
default=True,
description=(
"If enabled, snapping will be enabled on new sessions.\n" "It is recommended to keep this `Enabled`"
),
description="If enabled, snapping will be enabled on new sessions.\nIt is recommended to keep this `Enabled`",
)
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
tmp_dir: StringProperty(
@@ -462,10 +428,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
max=100,
subtype="PERCENTAGE",
name="Non-Openings Opacity",
description=(
"When modifying openings, other elements of the model will display with some transparency.\n"
"0 is fully transparent and 100 is fully opaque"
),
description="When modifying openings, other elements of the model will display with some transparency.\n0 is fully transparent and 100 is fully opaque",
)
bsdd_load_preview_dictionaries: BoolProperty(
@@ -535,7 +498,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_setup_workspace: bool
activate_workspace: bool
should_setup_toolbar: bool
should_use_snap: bool
should_play_chaching_sound: bool
tmp_dir: str
decorations_colour: tuple[float, float, float, float]
+3 -11
View File
@@ -60,18 +60,10 @@ def edit_assigned_product(
) -> None:
element = ifc.get_entity(obj)
assert element
# Temporarily accomodate existing bug and check for multiple products.
existing_products = drawing.get_assigned_product_workaround(element)
if existing_products != [product]:
if product in existing_products:
assert product is not None
existing_products.remove(product)
for existing_product in existing_products:
existing_product = drawing.get_assigned_product(element)
if existing_product != product:
if existing_product:
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
if product:
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
+4 -5
View File
@@ -147,7 +147,7 @@ def remove_representation(
assert element
element_type = geometry.get_element_type(element)
data = None
data_removed_by_switch_representation = False
has_switched_from_data = False
if element_type and (geometry.is_mapped_representation(representation) or geometry.is_type_product(element)):
representation = geometry.resolve_mapped_representation(representation)
data = geometry.get_representation_data(representation)
@@ -155,7 +155,7 @@ def remove_representation(
for element in geometry.get_elements_of_type(element_type):
obj = ifc.get_object(element)
if obj:
data_removed_by_switch_representation = True
has_switched_from_data = True
geometry.switch_from_representation(obj, representation)
obj = ifc.get_object(element_type)
if obj:
@@ -164,13 +164,12 @@ def remove_representation(
else:
data = geometry.get_representation_data(representation)
if data and geometry.has_data_users(data):
data_removed_by_switch_representation = True
has_switched_from_data = True
geometry.switch_from_representation(obj, representation)
ifc.run("geometry.unassign_representation", product=element, representation=representation)
ifc.run("geometry.remove_representation", representation=representation)
if data and not data_removed_by_switch_representation:
if data and not has_switched_from_data:
geometry.delete_data(data)
+2 -1
View File
@@ -70,7 +70,8 @@ def assign_class(
ifc_representation_class: Optional[str] = None,
) -> Optional[ifcopenshell.entity_instance]:
"""
:param context: is not optional if ``should_add_representation`` is True
Args:
context: is not optional if `should_add_representation` is True
TODO: Do NOT use should_add_representation. Because it internally calls
geometry.add_representation which is 1,000 lines of Blender -> IFC magic.
+2 -15
View File
@@ -308,7 +308,6 @@ class Document:
class Drawing:
def activate_drawing(cls, camera): pass
def add_literal(cls, **attributes): pass
def clear_annotation_relationships(cls, drawing): pass
def copy_representation(cls, source, dest): pass
def create_annotation_context(cls, target_view, object_type=None): pass
def create_annotation_object(cls, drawing, object_type): pass
@@ -343,7 +342,6 @@ class Drawing:
def get_annotation_context(cls, target_view, object_type=None): pass
def get_annotation_representation(cls, element_type): pass
def get_assigned_product(cls, element): pass
def get_assigned_product_workaround(cls, element): pass
def get_body_context(cls): pass
def get_default_drawing_path(cls, name): pass
def get_default_drawing_resource_path(cls, resource): pass
@@ -487,19 +485,6 @@ class Georeference:
def xyz2enh(cls, coordinates): pass
@interface
class Group:
def get_group_props(cls): pass
def get_groups_data(cls, group_type): pass
def import_groups(cls, group_type): pass
def enable_group_editing_ui(cls) : pass
def disable_group_editing_ui(cls): pass
def disable_editing_group(cls): pass
def set_active_group_to_edit(cls, group): pass
def toggle_group(cls, group, group_type, option): pass
def update_uilist_index(cls, group_type): pass
@interface
class Ifc:
def get(cls): pass
@@ -869,6 +854,8 @@ class Sequence:
def get_animation_product_frames(cls, work_schedule, settings): pass
def get_animation_settings(cls): pass
def get_checked_tasks(cls): pass
def get_direct_nested_tasks(cls, task):pass
def get_direct_task_outputs(cls, task): pass
def get_finish_date(cls): pass
def get_highlighted_task(cls): pass
def get_lag_time_attributes(cls): pass
+9 -19
View File
@@ -100,7 +100,6 @@ class Blender(bonsai.core.tool.Blender):
- (identifier, name, description, icon, number)
"""
BLENDER_ENUM_ITEMS = Iterable[BLENDER_ENUM_ITEM]
BLENDER_5 = bpy.app.version >= (5, 0, 0)
@classmethod
def activate_camera(cls, obj: bpy.types.Object) -> None:
@@ -417,7 +416,7 @@ class Blender(bonsai.core.tool.Blender):
cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {}
) -> Union[bpy.types.ShaderNode, None]:
"""returns first node from the `blender_material` shader graph with type `node_type`"""
if not tool.Style.get_use_nodes(blender_material):
if not blender_material.use_nodes:
return
nodes = blender_material.node_tree.nodes
for node in nodes:
@@ -710,7 +709,7 @@ class Blender(bonsai.core.tool.Blender):
new_selected_objects = [o for o in selected_objects if cls.is_valid_data_block(o) and o in view_layer_objects]
if active_object and not cls.is_valid_data_block(active_object):
if active_object and (not cls.is_valid_data_block(active_object) or active_object not in view_layer_objects):
active_object = None
return context, active_object, new_selected_objects
@@ -1065,7 +1064,7 @@ class Blender(bonsai.core.tool.Blender):
"""Tries to validate the current BIM modifier parameters for the active object
Goes into path editing mode if the modifier supports it
:return: True if an action was taken, False otherwise
Returns True if an action was taken, False otherwise
"""
if cls.is_roof(element):
if cls.is_editing_roof_parameters(obj):
@@ -1089,7 +1088,7 @@ class Blender(bonsai.core.tool.Blender):
def try_canceling_editing_modifier_parameters_or_path(cls, obj: bpy.types.Object) -> bool:
"""Tries to cancel the current BIM modifier parameters or path edition for the active object
:return: True if an action was taken, False otherwise
Returns True if an action was taken, False otherwise
"""
if cls.is_editing_railing_path(obj):
bpy.ops.bim.cancel_editing_railing_path()
@@ -1672,10 +1671,12 @@ class Blender(bonsai.core.tool.Blender):
- "user_interface.wcol_menu.text" (Menu Text)
- "user_interface.wcol_menu.text_sel" (Menu Text Selected)
:param color_path: The attribute path relative to bpy.context.preferences.themes[0].
:param threshold: The RGB sum threshold for determining dark mode. Default is 1.671.
Args:
color_path (str, optional): The attribute path relative to bpy.context.preferences.themes[0].
threshold (float, optional): The RGB sum threshold for determining dark mode. Default is 1.671.
:return: 'dm' (dark mode) if the RGB sum is > threshold, otherwise 'lm' (light mode).
Returns:
str: 'dm' (dark mode) if the RGB sum is > threshold, otherwise 'lm' (light mode).
"""
full_path = f"bpy.context.preferences.themes[0].{color_path}"
@@ -2082,14 +2083,3 @@ class Blender(bonsai.core.tool.Blender):
assert bpy.context.preferences
if props_updated and bpy.context.preferences.use_preferences_save:
bpy.ops.wm.save_userpref()
@classmethod
def get_eevee_name(cls) -> Literal["BLENDER_EEVEE"] | Literal["BLENDER_EEVEE_NEXT"]:
"""Convenience method to get correct eevee render engine name in multiple Blender versions.
In Blender 4.2 eevee was renamed to "eevee next".
In Blender 5 eevee next is now just "eevee" again.
"""
if cls.BLENDER_5:
return "BLENDER_EEVEE"
return "BLENDER_EEVEE_NEXT"
+20 -208
View File
@@ -33,7 +33,6 @@ import sys
import bpy
import math
import bmesh
import numpy as np
import mathutils.geometry
from mathutils import Vector, Matrix, geometry
import itertools
@@ -189,33 +188,6 @@ class Cad:
"""
return geometry.intersect_line_plane(v1, v2, plane_co, plane_no)
def intersect_edge_plane_v2(v1, v2, plane_co, plane_no, eps=1e-9):
"""
Numpy version of intersect_edge_plane
> takes an edges as two vector, and a plane as origin point and normal
< return the intersection point or None
"""
# References: https://rosettacode.org/wiki/Find_the_intersection_of_a_line_with_a_plane#Python,
# https://stackoverflow.com/a/18543221
p0 = np.array((v1.x, v1.y, v1.z), dtype=np.float64)
p1 = np.array((v2.x, v2.y, v2.z), dtype=np.float64)
pc = np.array((plane_co.x, plane_co.y, plane_co.z), dtype=np.float64)
n = np.array((plane_no.x, plane_no.y, plane_no.z), dtype=np.float64)
u = p1 - p0
dot = np.dot(u, n)
if abs(dot) < eps:
# Line is parallel to plane (no intersection or lies in plane)
return None
w = pc - p0
fac = np.dot(w, n) / dot
p = p0 + fac * u
return Vector(p)
@classmethod
def intersect_edges(
cls, edge1: tuple[Vector, Vector], edge2: tuple[Vector, Vector]
@@ -243,50 +215,47 @@ class Cad:
def intersect_edges_v2(cls, edge1, edge2):
"""
Calculate the closest points on two line segments.
Note: This function doesn't use intersect_line_line and uses Numpy for calculations
Note: This function doesn't use intersect_line_line
> edge1: tuple of two vectors (v1, v2) representing the first segment
> edge2: tuple of two vectors (v3, v4) representing the second segment
< returns: tuple of two vectors (C1, C2) or (None, None) if lines are parallel
"""
# This function seems to work better than intersect_line_line
# This function seems to work better then intersect_line_line
# in orthogonal view
# https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
is_2d = False
# Starting and ending points
p1, p1_end = edge1
p2, p2_end = edge2
if len(p1) == 2:
P1, P1_end = edge1
P2, P2_end = edge2
if len(P1) == 2:
is_2d = True
p1, p1_end = p1.to_3d(), p1_end.to_3d()
p2, p2_end = p2.to_3d(), p2_end.to_3d()
P1, P1_end = P1.to_3d(), P1_end.to_3d()
P2, P2_end = P2.to_3d(), P2_end.to_3d()
p1 = np.array((p1.x, p1.y, p1.z), dtype=np.float64)
p1_end = np.array((p1_end.x, p1_end.y, p1_end.z), dtype=np.float64)
p2 = np.array((p2.x, p2.y, p2.z), dtype=np.float64)
p2_end = np.array((p2_end.x, p2_end.y, p2_end.z), dtype=np.float64)
# Directions
d1 = p1_end - p1
d2 = p2_end - p2
d1 /= np.linalg.norm(d1) or 1 # equivalent of Vector.normalized() or Vector / Vector.length
d2 /= np.linalg.norm(d2) or 1
d1 = (P1_end - P1).normalized()
d2 = (P2_end - P2).normalized()
n = np.cross(d1, d2)
n = d1.cross(d2)
# if n is zero, lines are parallel
if abs(np.linalg.norm(n)) < 1e-6:
if abs(n.length) < 1e-6:
return None, None
n2 = np.cross(d2, n)
c1 = p1 + (np.dot((p2 - p1), n2) / (np.dot(d1, n2))) * d1
n1 = np.cross(d1, n)
c2 = p2 + (np.dot((p1 - p2), n1) / (np.dot(d2, n1))) * d2
n2 = d2.cross(n)
C1 = P1 + ((P2 - P1).dot(n2) / (d1.dot(n2))) * d1
n1 = d1.cross(n)
C2 = P2 + ((P1 - P2).dot(n1) / (d2.dot(n1))) * d2
if is_2d:
return Vector(c1[:2].copy()), Vector(c2[:2].copy())
return C1.to_2d(), C2.to_2d()
else:
return Vector(c1), Vector(c2)
return C1, C2
@classmethod
def get_intersection(cls, edge1, edge2):
@@ -838,160 +807,3 @@ class Cad:
v1 = v2
return new_verts
@classmethod
def region_2d_to_vector_3d_np(cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector) -> Vector:
"""
Numpy version of view3d_utils.region_2d_to_vector_3d
Return a direction vector from the viewport at the specific 2d region
coordinate.
> region: region of the 3D viewport, typically bpy.context.region.
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
> coord: 2d coordinates relative to the region:
(event.mouse_region_x, event.mouse_region_y) for example.
< returns a normalized 3d vector.
"""
view_m = np.array(rv3d.view_matrix)
window_m = np.array(rv3d.window_matrix)
viewinv = np.linalg.inv(view_m)
if rv3d.is_perspective:
# For better precision with large numbers, avoid using rv3d.perspective_matrix. See https://github.com/IfcOpenShell/IfcOpenShell/issues/7046
# Calculate it with view_matrix and window_matrix as numpy arrays.
pers_m = window_m @ view_m
persinv = np.linalg.inv(pers_m)
out = np.array(
[(2.0 * coord[0] / region.width) - 1.0, (2.0 * coord[1] / region.height) - 1.0, -0.5, 1.0],
dtype=np.float64,
)
w = out[:3].dot(persinv[3, :3]) + persinv[3, 3]
viewinv_translation = viewinv[:3, 3]
view_vector = (persinv.dot(out)[:3] / w) - viewinv_translation
else:
view_vector = -viewinv[:3, 2].copy() # -Z column
view_vector /= np.linalg.norm(view_vector) or 1 # equivalent to Vector.normalized()
return Vector(view_vector)
@classmethod
def region_2d_to_location_3d_np(
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, depth_location: Vector
) -> Vector:
"""
Numpy version of view3d_utils.region_2d_to_location_3d
Return a 3d location from the region relative 2d coords, aligned with
*depth_location*.
> region: region of the 3D viewport, typically bpy.context.region.
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
> coord: 2d coordinates relative to the region:
(event.mouse_region_x, event.mouse_region_y) for example.
< returns a normalized 3d vector.
"""
coord_vec = cls.region_2d_to_vector_3d_np(region, rv3d, coord)
depth_location = np.array([depth_location[0], depth_location[1], depth_location[2]], dtype=np.float64)
origin_start = cls.region_2d_to_origin_3d_np(region, rv3d, coord)
origin_end = origin_start + coord_vec
if rv3d.is_perspective:
viewinv = np.linalg.inv(rv3d.view_matrix)
view_vec = viewinv[:3, 2].copy()
return cls.intersect_edge_plane_v2(
Vector(origin_start), Vector(origin_end), Vector(depth_location), Vector(view_vec)
)
else:
return cls.point_on_edge(
Vector(depth_location),
(Vector(origin_start), Vector(origin_end)),
)
@classmethod
def region_2d_to_origin_3d_np(
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, *, clamp: float = None
) -> Vector:
"""
Numpy version of view3d_utils.region_2d_to_origin_3d
Return the 3d view origin from the region relative 2d coords.
.. note::
Orthographic views have a less obvious origin,
the far clip is used to define the viewport near/far extents.
Since far clip can be a very large value,
the result may give with numeric precision issues.
To avoid this problem, you can optionally clamp the far clip to a
smaller value based on the data you're operating on.
> region: region of the 3D viewport, typically bpy.context.region.
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
> coord: 2d coordinates relative to the region:
(event.mouse_region_x, event.mouse_region_y) for example.
> clamp: clamp: Clamp the maximum far-clip value used.
(negative value will move the offset away from the view_location)
< returns the origin of the viewpoint in 3d space.
"""
view_m = np.array(rv3d.view_matrix)
window_m = np.array(rv3d.window_matrix)
viewinv = np.linalg.inv(view_m)
if rv3d.is_perspective:
origin_start = viewinv[:3, 3].copy()
else:
pers_m = window_m @ view_m # See https://github.com/IfcOpenShell/IfcOpenShell/issues/7046
persinv = np.linalg.inv(pers_m)
dx = (2.0 * coord[0] / region.width) - 1.0
dy = (2.0 * coord[1] / region.height) - 1.0
origin_start = (persinv[:3, 0] * dx) + (persinv[:3, 1] * dy) + persinv[:3, 3]
if clamp != 0.0:
if rv3d.view_perspective != "CAMERA":
origin_offset = persinv[:3, 2].copy() # column 2
if clamp is not None:
c = float(clamp)
if c < 0.0:
origin_offset = -origin_offset
c = -c
length = np.linalg.norm(origin_offset)
if length > c and length > 0.0:
origin_offset = (origin_offset / length) * c
origin_start = origin_start - origin_offset
return Vector(origin_start)
@classmethod
def location_3d_to_region_2d_np(
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, *, default=None
) -> Vector:
"""
Numpy version of view3d_utils.location_3d_to_region_2d
Return the *region* relative 2d location of a 3d position.
> region: region of the 3D viewport, typically bpy.context.region.
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
> coord: 2d coordinates relative to the region:
(event.mouse_region_x, event.mouse_region_y) for example.
< returns a 2d location.
"""
pt = np.array((coord[0], coord[1], coord[2], 1.0), dtype=np.float64)
view_m = np.array(rv3d.view_matrix)
window_m = np.array(rv3d.window_matrix)
pers_m = window_m @ view_m
prj = pers_m.dot(pt) # 4-vector
w = prj[3]
if w > 0.0:
width_half = region.width / 2.0
height_half = region.height / 2.0
x = width_half + width_half * (prj[0] / w)
y = height_half + height_half * (prj[1] / w)
return Vector((float(x), float(y)))
return default
+6 -5
View File
@@ -143,11 +143,12 @@ class Debug(bonsai.core.tool.Debug):
Note that Styles UI (or other UI) should be updated manually after using this method.
:param object_type: The type of object to merge
:param by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
Args:
object_type: The type of object to merge
by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
"""
def normalize_name(name: str) -> str:
+6 -57
View File
@@ -38,15 +38,12 @@ import bonsai.core.geometry
import bonsai.core.type
import bonsai.tool as tool
import ifcopenshell.api
import ifcopenshell.api.geometry
import ifcopenshell.api.context
import ifcopenshell.api.drawing
import ifcopenshell.api.document
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.geom
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import ifcopenshell.util.representation
import ifcopenshell.util.element
import ifcopenshell.util.selector
@@ -223,7 +220,6 @@ class Drawing(bonsai.core.tool.Drawing):
def exclude_annotation_from_drawing(
cls, element: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance
) -> None:
ifc_file = tool.Ifc.get()
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=drawing, name="EPset_Drawing")
@@ -897,33 +893,6 @@ class Drawing(bonsai.core.tool.Drawing):
return axis
return product
@classmethod
def get_assigned_product_workaround(
cls, element: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
"""Get all products assigned to the element.
A workaround allowing to unassign accumulated products until we properly resolve #4014.
In theory annotations should have more than one product assigned,
but there's still undefined bug causing that in some cases.
"""
assigned_products: list[ifcopenshell.entity_instance] = []
for rel in element.HasAssignments:
if not rel.is_a("IfcRelAssignsToProduct"):
continue
assigned_products.append(rel.RelatingProduct)
if len(assigned_products) > 1:
print(
f"WARNING. Detected multiple assigned products ({len(assigned_products)}) for annotation '{element}'."
"\nIf you can reproduce this, please report it to Bonsai developers "
"at https://github.com/IfcOpenShell/IfcOpenShell/issues/4014."
"\nAssigned products:\n" + "\n".join([str(p) for p in assigned_products])
)
return assigned_products
@classmethod
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
elements = set(
@@ -1736,22 +1705,11 @@ class Drawing(bonsai.core.tool.Drawing):
im = camera.matrix_world.inverted()
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in [v1, v2]]
target_view = cls.get_drawing_target_view(drawing)
bounds = helper.ortho_view_frame(camera.data)
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"):
# For plan views, clip to XY bounds and set Z=0
if not (points := helper.clip_segment(bounds, [v1, v2])):
return
for v in points:
v.z = 0
elif target_view in ("ELEVATION_VIEW", "SECTION_VIEW"):
# For section/elevation views, elevate the segment vertically
if not (points := helper.elevate_segment(bounds, [v1, v2])):
return
else:
if not (points := helper.clip_segment(bounds, [v1, v2])):
return
for v in points:
v.z = 0
return points
@classmethod
@@ -2011,18 +1969,9 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def get_default_annotation_matrix(cls, camera, matrix_world=None):
if matrix_world is None:
# Use normalized camera matrix (without scale) for RCP compatibility
matrix_world = cls.get_camera_matrix(camera)
# Check if this is an RCP (reflected ceiling plan) by checking for negative scale
camera_scale = camera.matrix_world.to_scale()
is_rcp = camera_scale.x < 0 or camera_scale.y < 0 or camera_scale.z < 0
# For RCP, the offset needs to be positive instead of negative
# because the Z axis is flipped 180° in get_camera_matrix
offset_direction = 1 if is_rcp else -1
annotation_offset = Vector((0, 0, offset_direction * (camera.data.clip_start + 0.05)))
annotation_offset = matrix_world.to_quaternion() @ annotation_offset
matrix_world = camera.matrix_world.copy()
annotation_offset = Vector((0, 0, -camera.data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
matrix_world.translation += annotation_offset
return matrix_world
+4 -11
View File
@@ -1146,9 +1146,8 @@ class Geometry(bonsai.core.tool.Geometry):
def record_object_position(cls, obj: bpy.types.Object) -> None:
# These are recorded separately because they have different numerical tolerances
props = tool.Blender.get_object_bim_props(obj)
# Explicit dtype for Blender <5.0 compatibility.
props.location_checksum = repr(np.array(obj.matrix_world.translation, dtype=np.float32).tobytes())
props.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
props.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
props.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
@classmethod
def remove_connection(cls, connection: ifcopenshell.entity_instance) -> None:
@@ -1262,7 +1261,7 @@ class Geometry(bonsai.core.tool.Geometry):
if tool.Ifc.get().schema == "IFC2X3":
return False
for slot in obj.material_slots:
if slot.material and tool.Style.get_use_nodes(slot.material):
if slot.material and slot.material.use_nodes:
for node in slot.material.node_tree.nodes:
if node.type == "TEX_COORD" and node.outputs["UV"].links:
return True
@@ -1360,8 +1359,6 @@ class Geometry(bonsai.core.tool.Geometry):
If no other representation present, will replace object with an empty.
Method assumes that `obj` does have a current representation (it could be not `representation`).
Will clean up old ``obj.data`` if no other users exist.
"""
element = tool.Ifc.get_entity(obj)
assert element
@@ -1380,11 +1377,7 @@ class Geometry(bonsai.core.tool.Geometry):
# `representation` is the only representation for object.
if new_representation is None:
old_data = obj.data
assert old_data is not None
cls.recreate_object_with_data(obj, None)
if not cls.has_data_users(old_data):
cls.delete_data(old_data)
return
bonsai.core.geometry.switch_representation(
@@ -2027,7 +2020,7 @@ class Geometry(bonsai.core.tool.Geometry):
continue
if not (style := tool.Ifc.get_entity(material)):
style = ifcopenshell.api.style.add_style(ifc_file, name=material.name)
if tool.Style.get_use_nodes(material):
if material.use_nodes:
ifc_class = "IfcSurfaceStyleRendering"
attributes = tool.Style.get_surface_rendering_attributes(material)
else:
+2 -2
View File
@@ -110,11 +110,11 @@ class Ifc(bonsai.core.tool.Ifc):
oprops = tool.Blender.get_object_bim_props(obj)
if not oprops.location_checksum:
return True # Let's be conservative
loc_check = np.frombuffer(eval(oprops.location_checksum), dtype=np.float32)
loc_check = np.frombuffer(eval(oprops.location_checksum))
loc_real = np.array(obj.matrix_world.translation).flatten()
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
return True
rot_check = np.frombuffer(eval(oprops.rotation_checksum), dtype=np.float32).reshape(3, 3)
rot_check = np.frombuffer(eval(oprops.rotation_checksum)).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3())
rot_dot = np.dot(rot_check, rot_real.T)
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
+3 -3
View File
@@ -116,7 +116,7 @@ class Loader(bonsai.core.tool.Loader):
if transparency := surface_style.get("Transparency", None):
alpha = 1 - transparency
blender_material.diffuse_color = surface_style["SurfaceColour"] + (alpha,)
tool.Style.set_use_nodes(blender_material, False)
blender_material.use_nodes = False
@classmethod
def restart_material_node_tree(cls, blender_material: bpy.types.Material) -> None:
@@ -204,7 +204,7 @@ class Loader(bonsai.core.tool.Loader):
# TODO: reset pins to default values if no values passed
if reflectance_method in ["PHYSICAL", "NOTDEFINED"]:
tool.Style.set_use_nodes(blender_material, True)
blender_material.use_nodes = True
cls.restart_material_node_tree(blender_material)
bsdf = tool.Blender.get_material_node(blender_material, "BSDF_PRINCIPLED")
assert bsdf
@@ -231,7 +231,7 @@ class Loader(bonsai.core.tool.Loader):
blender_material.blend_method = "BLEND"
elif reflectance_method == "FLAT":
tool.Style.set_use_nodes(blender_material, True)
blender_material.use_nodes = True
cls.restart_material_node_tree(blender_material)
output = tool.Blender.get_material_node(blender_material, "OUTPUT_MATERIAL")
+48 -93
View File
@@ -44,8 +44,11 @@ import mathutils
from math import atan, cos, degrees, pi, radians
from mathutils import Matrix, Vector
from copy import deepcopy
from functools import partial
from bonsai.bim import import_ifc
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData
from bonsai.bim.module.model.opening import FilledOpeningGenerator
from ifcopenshell.util.shape_builder import ShapeBuilder, np_to_3d
from typing import Optional, Union, TypeVar, Any, Literal, TYPE_CHECKING, TypedDict, assert_never
from collections.abc import Iterable, Sequence
@@ -142,10 +145,9 @@ class Model(bonsai.core.tool.Model):
for prop_name in data:
if prop_name in non_si_props:
continue
# `None` is used by `custom_first_last_tread_run`.
prop_value: Iterable[float | None] | float = data[prop_name]
prop_value = data[prop_name]
if isinstance(prop_value, collections.abc.Iterable):
data[prop_name] = [v if v is None else v * si_conversion for v in prop_value]
data[prop_name] = [v * si_conversion for v in prop_value]
else:
data[prop_name] = prop_value * si_conversion
return data
@@ -1101,7 +1103,6 @@ class Model(bonsai.core.tool.Model):
return
from PIL import Image, ImageDraw
from bonsai.bim.module.model.data import AuthoringData
obj = tool.Ifc.get_object(element)
if not obj:
@@ -1207,8 +1208,6 @@ class Model(bonsai.core.tool.Model):
Allows postponing the thumbnail update until it is actually needed by the user.
"""
from bonsai.bim.module.model.data import AuthoringData
element_id = element.id()
if element_id not in AuthoringData.type_thumbnails:
return
@@ -1312,34 +1311,23 @@ class Model(bonsai.core.tool.Model):
@classmethod
def is_parametric_roof_active(cls) -> bool:
from bonsai.bim.module.model.data import RoofData
return bool((RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"])
@classmethod
def is_parametric_railing_active(cls) -> bool:
from bonsai.bim.module.model.data import RailingData
return bool((RailingData.is_loaded or not RailingData.load()) and RailingData.data["pset_data"])
@classmethod
def is_parametric_window_active(cls) -> bool:
from bonsai.bim.module.model.data import WindowData
return bool((WindowData.is_loaded or not WindowData.load()) and WindowData.data["pset_data"])
@classmethod
def is_parametric_door_active(cls) -> bool:
from bonsai.bim.module.model.data import DoorData
return bool((DoorData.is_loaded or not DoorData.load()) and DoorData.data["pset_data"])
CustomTreadRunType = Union[tuple[float, float], tuple[None, None]]
@classmethod
def get_active_stair_calculated_params(cls, pset_data: Optional[dict[str, Any]] = None) -> dict[str, Any]:
assert (obj := bpy.context.active_object)
props = tool.Model.get_stair_props(obj)
props = bpy.context.active_object.BIMStairProperties
if props.is_editing:
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -1350,48 +1338,38 @@ class Model(bonsai.core.tool.Model):
last_tread_run = props.custom_first_last_tread_run[1] / si_conversion
nosing_length = props.nosing_length / si_conversion
else:
assert pset_data
number_of_treads: int = pset_data["number_of_treads"]
height: float = pset_data["height"]
tread_run: float = pset_data["tread_run"]
number_of_treads = pset_data["number_of_treads"]
height = pset_data["height"]
tread_run = pset_data["tread_run"]
# use .get to not break the old .ifc models
custom_first_last_tread_run: tool.Model.CustomTreadRunType = pset_data.get(
"custom_first_last_tread_run", (0, 0)
)
custom_first_last_tread_run = pset_data.get("custom_first_last_tread_run", (0, 0))
first_tread_run, last_tread_run = custom_first_last_tread_run
nosing_length = pset_data.get("nosing_length", 0)
calculated_params: dict[str, Any] = {}
calculated_params = {}
number_of_rises = number_of_treads + 1
calculated_params["Number of Risers"] = number_of_rises
calculated_params["Tread Rise"] = round(height / number_of_rises, 5)
# calculate stair length
# Start with all treads using default tread_run
n_default_tread_runs = number_of_rises
length = 0.0
# If first tread has custom width (non-None), use it instead of default.
if first_tread_run is not None:
length = 0
if first_tread_run != 0:
n_default_tread_runs -= 1
length += first_tread_run
# If last tread has custom width (non-None), use it instead of default
if last_tread_run is not None:
if last_tread_run != 0:
n_default_tread_runs -= 1
length += last_tread_run
# Add remaining default tread runs
length += tread_run * n_default_tread_runs
# Handle nosing length effects on total length
# Nosing overlaps don't affect tread run spacing,
# but the first tread's nosing extends the total length
if n_default_tread_runs >= 0:
length += last_tread_run
length += tread_run * max(n_default_tread_runs, 0)
# nosing overlaps
# are not part of the tread run
# so they don't affect the stair length
# except the first tread's nosing
if nosing_length > 0: # nosing overlaps
length += nosing_length
if nosing_length < 0: # tread gaps between treads
if nosing_length < 0: # tread gaps
length += abs(nosing_length) * number_of_treads
calculated_params["Length"] = round(length, 5)
pitch = height / length
pitch_formatted = str(round(pitch * 100, 1)) + " % / " + str(round(degrees(atan(pitch)), 1)) + " deg"
@@ -1399,38 +1377,35 @@ class Model(bonsai.core.tool.Model):
return calculated_params
StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"]
@classmethod
def generate_stair_2d_profile(
cls,
number_of_treads: int,
height: float,
width: float,
tread_run: float,
stair_type: StairType,
number_of_treads,
height,
width,
tread_run,
stair_type,
# WOOD/STEEL CONCRETE STAIR ARGUMENTS
tread_depth: Union[float, None] = None,
tread_depth=None,
# CONCRETE STAIR ARGUMENTS
has_top_nib: Union[bool, None] = None,
top_slab_depth: Union[float, None] = None,
base_slab_depth: Union[float, None] = None,
custom_first_last_tread_run: Union[tuple[float, float], tuple[None, None]] = (None, None),
nosing_length: float = 0.0,
has_top_nib=None,
top_slab_depth=None,
base_slab_depth=None,
custom_first_last_tread_run=(0, 0),
nosing_length=0,
# CONCRETE GENERIC STAIR ARGUMENTS
nosing_depth: float = 0.0,
) -> tuple[list[Vector], list[tuple[int, ...]], list[[list[int]]]]:
nosing_depth=0,
):
"""returns a tuple of stair profile data: (vertices, edges, faces)"""
vertices: list[Vector] = []
edges: list[tuple[int, ...]] = []
faces: list[[list[int]]] = []
vertices = []
edges = []
faces = []
number_of_risers = number_of_treads + 1
tread_rise = height / number_of_risers
nosing_overlap = max(nosing_length, 0)
nosing_tread_gap = -min(nosing_length, 0)
nosing_overlap_offset = -V_(nosing_overlap, 0)
first_tread_run = custom_first_last_tread_run[0] if custom_first_last_tread_run[0] is not None else tread_run
def define_generic_stair_treads():
vertices.append(Vector([0, 0]))
@@ -1446,12 +1421,7 @@ class Model(bonsai.core.tool.Model):
elif nosing_depth == 0:
default_tread_verts = (V_(-nosing_overlap, tread_rise), V_(tread_run, tread_rise))
else: # nosing_overlap > 0 nosing_depth > 0
# kind of L shape:
# (2)●───────────────────────────●(3)
# |
# |
# ●──────────────●
# (1) (0)
# kind of L shape
default_tread_verts = (
V_(0, tread_rise - nosing_depth),
V_(-nosing_overlap, tread_rise - nosing_depth),
@@ -1500,12 +1470,12 @@ class Model(bonsai.core.tool.Model):
current_offset += tread_offset
if stair_type == "WOOD/STEEL":
assert tread_depth is not None
builder = ShapeBuilder(None)
# full tread rectangle
def get_tread_verts(size: Vector) -> list[Vector]:
coords = ShapeBuilder.get_rectangle_coords(position=V_(0, -(tread_depth - tread_rise)), size=size)
return [Vector(x) for x in coords]
def get_tread_verts(*args, **kwargs):
fn = partial(builder.get_rectangle_coords, position=V_(0, -(tread_depth - tread_rise)))
return [Vector(x) for x in fn(*args, **kwargs)]
default_tread_verts = get_tread_verts(size=V_(tread_run + nosing_overlap, tread_depth))
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
@@ -1534,7 +1504,6 @@ class Model(bonsai.core.tool.Model):
# each tread is a separate shape
cur_offset = V_(0, 0)
tread_index = 0
for i in range(number_of_risers):
tread_offset, tread_verts = get_tread_data(i)
@@ -1569,11 +1538,6 @@ class Model(bonsai.core.tool.Model):
elif stair_type == "CONCRETE":
define_generic_stair_treads()
assert has_top_nib is not None
assert top_slab_depth is not None
assert base_slab_depth is not None
assert tread_depth is not None
# add the nibs
# basically we define stair bottom line as a line at `tread_depth` distance
# from the tread diagonal line
@@ -1587,20 +1551,15 @@ class Model(bonsai.core.tool.Model):
# s0 is just a sampled point from the bottom line
# we stick to the third point as the first point
# is affected by customized tread run
s0 = V_(first_tread_run, tread_rise) + td_vector
s0 = V_(custom_first_last_tread_run[0] or tread_run, tread_rise) + td_vector
# comes from y = stair_tan * x + b
b = s0.y - stair_tan * s0.x
def get_point_on_2d_line(
x: Union[float, None] = None,
y: Union[float, None] = None,
) -> Vector:
if x is not None and y is None:
def get_point_on_2d_line(x=None, y=None):
if y is None:
y = stair_tan * x + b
elif x is None and y is not None:
elif x is None:
x = (y - b) / stair_tan
else:
assert False
return V_(x, y)
# top nib
@@ -1644,13 +1603,11 @@ class Model(bonsai.core.tool.Model):
else:
raise Exception(f"Unsupported stair type: {stair_type}")
vertices = [v.to_3d().xzy for v in vertices]
vertices = (v.to_3d().xzy for v in vertices)
return (vertices, edges, faces)
@classmethod
def update_simple_openings(cls, element: ifcopenshell.entity_instance) -> None:
from bonsai.bim.module.model.opening import FilledOpeningGenerator
ifc_file = tool.Ifc.get()
fillings = {e: tool.Ifc.get_object(e) for e in tool.Ifc.get_all_element_occurrences(element)}
@@ -2213,8 +2170,6 @@ class Model(bonsai.core.tool.Model):
@classmethod
def add_filled_opening(cls, voided_obj: bpy.types.Object, filling_obj: bpy.types.Object) -> None:
from bonsai.bim.module.model.opening import FilledOpeningGenerator
FilledOpeningGenerator().generate(filling_obj, voided_obj)
@classmethod
+3 -4
View File
@@ -172,24 +172,23 @@ class Polyline(bonsai.core.tool.Polyline):
angle = tool.Cad.angle_3_vectors(
second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
)
angle_round_threshold = 1000 # Avoids rounding when distance is too big
# Round angle to the nearest 0.05
angle = round(angle / 0.05) * 0.05 if distance < angle_round_threshold else angle
angle = round(angle / 0.05) * 0.05
orientation_angle = tool.Cad.angle_3_vectors(
world_second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
)
# Round angle to the nearest 0.05
orientation_angle = round(orientation_angle / 0.05) * 0.05 if distance < angle_round_threshold else angle
orientation_angle = round(orientation_angle / 0.05) * 0.05
if distance == 0:
angle = 0
orientation_angle = 0
if input_ui:
if should_round:
angle = 5 * round(angle / 5) if distance < angle_round_threshold else angle
angle = 5 * round(angle / 5)
factor = tool.Snap.get_increment_snap_value(context)
distance = factor * round(distance / factor)
input_ui.set_value("X", mouse_vector.x)
-11
View File
@@ -18,7 +18,6 @@
from __future__ import annotations
import os
import shutil
import bpy
import ifcopenshell
import ifcopenshell.api.document
@@ -503,13 +502,3 @@ class Project(bonsai.core.tool.Project):
d2 = v3 - v2
normals.append((v1, d1.cross(d2).normalized()))
return normals
TEMP_PROJECT_PATH = Path.cwd() / "test/files/temp/test_project.ifc"
@classmethod
def save_test_project(cls) -> None:
tmp = cls.TEMP_PROJECT_PATH.parent
# Clean up previous test project to avoid conflicts with non-updated assets.
if tmp.exists():
shutil.rmtree(tmp)
bpy.ops.bim.save_project(filepath=cls.TEMP_PROJECT_PATH.__str__(), should_save_as=True)
+8 -13
View File
@@ -93,7 +93,7 @@ class Raycast(bonsai.core.tool.Raycast):
return None
for v in bbox:
coord_2d = tool.Cad.location_3d_to_region_2d_np(context.region, context.space_data.region_3d, v)
coord_2d = view3d_utils.location_3d_to_region_2d(context.region, context.space_data.region_3d, v)
if coord_2d is not None:
transposed_bbox.append(coord_2d)
@@ -180,11 +180,8 @@ class Raycast(bonsai.core.tool.Raycast):
if not mouse_pos:
mouse_pos = event.mouse_region_x, event.mouse_region_y
view_vector = tool.Cad.region_2d_to_vector_3d_np(region, rv3d, mouse_pos)
ray_origin = tool.Cad.region_2d_to_origin_3d_np(
region, rv3d, mouse_pos, clamp=10
) # TODO clamp is hardcoded but might be necessary to adapt
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, mouse_pos)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, mouse_pos)
ray_target = ray_origin + view_vector
ray_direction = ray_target - ray_origin
@@ -250,7 +247,7 @@ class Raycast(bonsai.core.tool.Raycast):
snap_threshold = cls.calculate_snap_threshold(rv3d.view_distance)
try:
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
except:
loc = Vector((0, 0, 0))
@@ -270,9 +267,7 @@ class Raycast(bonsai.core.tool.Raycast):
points.append(snap_point)
return points
if obj and obj.type == "CURVE":
mw = obj.matrix_world.copy()
obj = bpy.data.objects.new("new_object", obj.to_mesh().copy())
obj.matrix_world = mw @ obj.matrix_world
if not custom_bmesh:
bm = bmesh.new()
@@ -345,7 +340,7 @@ class Raycast(bonsai.core.tool.Raycast):
snap_threshold = cls.calculate_snap_threshold(rv3d.view_distance)
try:
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
except:
loc = Vector((0, 0, 0))
@@ -404,8 +399,8 @@ class Raycast(bonsai.core.tool.Raycast):
default_container_elevation = 0.0
intersection = Vector((0, 0, default_container_elevation))
try:
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
intersection = tool.Cad.intersect_edge_plane_v2(ray_target, loc, plane_origin, plane_normal)
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
intersection = mathutils.geometry.intersect_line_plane(ray_target, loc, plane_origin, plane_normal)
except:
intersection = Vector((0, 0, default_container_elevation))
@@ -423,7 +418,7 @@ class Raycast(bonsai.core.tool.Raycast):
snap_threshold = cls.calculate_snap_threshold(rv3d.view_distance)
try:
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
except:
loc = Vector((0, 0, 0))

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