Compare commits

..

7 Commits

Author SHA1 Message Date
Andrej730 e9d003c22f clear cgal cache 2025-10-30 19:20:37 +05:00
Andrej730 2e71e002fc try out just intel 2025-10-30 18:54:31 +05:00
Andrej730 40ed2fe3ab build-all.py - remove unused wasm flags
`-Wl,-undefined,suppress` have no meaning in emscripten and they are just ignored. And emscripten is ignoring undefined symbols by default without any flags.
2025-10-30 18:53:56 +05:00
Andrej730 379fe632a3 build-all.py - switch to dynamic_lookup
1) `-undefined,suppress` is deprecated and linking is sending warnings about it and it seems to be superseded by `dynamic_lookup`
2) `-flat_namespace` is not compatible with GMP when building for intel, for some reason and results in horrible errors during linking:

Example error:
```
ld: branch8 out of range 366651 in ___gmpn_add_nc
final section layout:
    __TEXT               addr=0x00000000, size=0x02d09000, fileOffset=0x00000000
        __text           addr=0x00001400, size=0x0264a5e4, fileOffset=0x00001400
        __stubs          addr=0x0264b9e4, size=0x0000423c, fileOffset=0x0264b9e4
        __stub_helper    addr=0x0264fc20, size=0x00006c58, fileOffset=0x0264fc20
        __gcc_except_tab addr=0x02656878, size=0x002efc04, fileOffset=0x02656878
        __const          addr=0x02946480, size=0x001cdc18, fileOffset=0x02946480
        __cstring        addr=0x02b140a0, size=0x0014324b, fileOffset=0x02b140a0
        __unwind_info    addr=0x02c572ec, size=0x0008d2d0, fileOffset=0x02c572ec
        __eh_frame       addr=0x02ce45c0, size=0x00024a30, fileOffset=0x02ce45c0
    __DATA_CONST         addr=0x02d09000, size=0x0022b000, fileOffset=0x02d09000
        __got            addr=0x02d09000, size=0x00001dc8, fileOffset=0x02d09000
        __mod_init_func  addr=0x02d0adc8, size=0x00000690, fileOffset=0x02d0adc8
        __const          addr=0x02d0b460, size=0x00227fd0, fileOffset=0x02d0b460
    __DATA               addr=0x02f34000, size=0x000c3000, fileOffset=0x02f34000
        __la_symbol_ptr  addr=0x02f34000, size=0x000056a0, fileOffset=0x02f34000
        __data           addr=0x02f396a0, size=0x0005f7b8, fileOffset=0x02f396a0
        __thread_vars    addr=0x02f98e58, size=0x000006d8, fileOffset=0x02f98e58
        __thread_ptrs    addr=0x02f99530, size=0x00000178, fileOffset=0x02f99530
        __thread_data    addr=0x02f996b0, size=0x000000d8, fileOffset=0x02f996b0
        __thread_bss     addr=0x02f99790, size=0x00018850, fileOffset=0x02f99790
        __bss            addr=0x02fb1fe0, size=0x0003b0c0, fileOffset=0x00000000
        __common         addr=0x02fed0a0, size=0x00009098, fileOffset=0x00000000
    __LINKEDIT           addr=0x02ff7000, size=0x0148e000, fileOffset=0x02fb2000
clang: error: linker command failed with exit code 1 (use -v to see invocation)
```
2025-10-30 18:53:55 +05:00
Andrej730 2ecaee9024 build-all.py - add VERBOSE=1 to the wrapper make command 2025-10-30 18:53:55 +05:00
Andrej730 f9a3c9a994 build-all.py - fix missing mac cross compilation host args in gmp
I guess something has changed in gmp 6.3.0 and this issue become more apparent.

Example error:
```
tmp-cnd_sub_n.s:133:14: error: tmp-cnd_add_n.sbrackets expression not supported on this target:
130 stp x8, x9, [x1,#16]
             ^
:2tmp-cnd_sub_n.s: :error: 134invalid instruction mnemonic 'bic':
6 bic x7, x13, x0:
error:  invalid instruction mnemonic 'cset'^
~Lrt: cset x0, cc~

     ^~~tmp-cnd_add_n.s~:
131:2: error: invalid instruction mnemonic 'adcs'
 adcs x8, x10, x6
 ^~~~
tmp-cnd_add_n.s:132:2: error: invalid instruction mnemonic 'adcs'
 adcs x9, x11, x7
 ^~~~
tmp-cnd_add_n.s:133:14: error: brackets expression not supported on this target
 stp x8, x9, [x1,#16]
             ^
tmp-cnd_add_n.s:134:6: error: invalid instruction mnemonic 'cset'
Lrt: cset x0, cs
     ^~~~
```
2025-10-29 14:38:24 +05:00
Andrej730 77acbff50b black . 2025-10-29 13:25:26 +05:00
236 changed files with 2333 additions and 9067 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"
+7 -10
View File
@@ -14,19 +14,15 @@ jobs:
runner: macos-14
arch: x64
oldarch:
- os: macos
runner: macos-14
arch: arm64
oldarch: m1
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
@@ -47,6 +43,7 @@ jobs:
- name: Unpack Dependencies
run: |
rm -rf ./build/*/*/*/install/*gmp* ./build/*/*/*/install/*cgal*
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
@@ -75,13 +72,13 @@ jobs:
/usr/local/bin/brew install gettext openssl
fi
set -o pipefail
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
python3 ./nix/build-all.py -v --diskcleanup ${MAC_INTEL} \
IFCOS_SCHEMAS=4 CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
python3 ./nix/build-all.py -v -py-313 --diskcleanup ${MAC_INTEL} \
| tee build.log
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: build-logs-osx-${{ matrix.arch }}
path: |
@@ -139,7 +136,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 -4
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,7 +42,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: build-logs-pyodide
path: |
@@ -77,7 +77,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
+3 -3
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"
+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
+5 -42
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,16 +64,13 @@ 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 }}
@@ -100,41 +97,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 +114,7 @@ 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 \
-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
@@ -1 +1 @@
0.8.5
0.8.4
+706 -29
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,13 +196,30 @@ if(BUILD_SHARED_LIBS)
set(IFCOPENSHELL_LIBRARY_DIR "${LIBDIR}")
endif()
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
UNIFY_ENVVARS_AND_CACHE(EIGEN_DIR)
if(NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(PYTHON_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(PYTHON_LIBRARY)
UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE)
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
endif()
# Get a list of all OPTION flags from the CMakeLists.txt
@@ -215,13 +230,53 @@ foreach(option_flag IN LISTS option_flags)
convert_env_var_to_bool("${option_flag}")
endforeach()
set(CMAKE_FIND_ROOT_PATH_BACKUP "${CMAKE_FIND_ROOT_PATH}")
macro(clear_wasm_sysroot)
if(WASM_BUILD)
# when using the nix/build-all.py build script we should not
# look into the sysroot for most of the dependencies but rather
# in the designated build/ folder created by the script.
set(CMAKE_FIND_ROOT_PATH "")
endif()
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
endmacro()
macro(restore_wasm_sysroot)
if(WASM_BUILD)
# reset to use sysroot
set(CMAKE_FIND_ROOT_PATH "${CMAKE_FIND_ROOT_PATH_BACKUP}")
endif()
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endmacro()
if(WITH_CGAL)
find_package(CGAL REQUIRED)
if(TARGET CGAL::CGAL)
if(NOT CGAL_INCLUDE_DIR)
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
# and sometiems it's getting in the way.
if(NOT DEFINED Boost_USE_STATIC_LIBS)
set(CGAL_Boost_USE_STATIC_LIBS OFF)
else()
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
endif()
find_package(CGAL REQUIRED)
if(NOT CGAL_DIR)
message(
FATAL_ERROR
"CGAL_SUPPORT enabled, but CGAL_INCLUDE_DIR wasn't provided and CGAL package couldn't be found."
)
endif()
message(STATUS "CGAL: found config at '${CGAL_DIR}'.")
link_libraries(CGAL::CGAL)
endif()
add_definitions(-DIFOPSH_WITH_CGAL)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
if(CITYJSON_SUPPORT)
add_definitions(-DIFOPSH_WITH_CITYJSON)
endif()
list(APPEND GEOMETRY_KERNELS cgal)
endif()
@@ -231,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)
@@ -243,7 +298,9 @@ if(GLTF_SUPPORT)
endif()
if(NOT nlohmann_json_DIR)
clear_wasm_sysroot()
find_path(json_header_path "nlohmann/json.hpp" HINTS ${JSON_INCLUDE_DIR})
restore_wasm_sysroot()
set(JSON_INCLUDE_DIR ${json_header_path})
if(json_header_path)
@@ -259,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)
@@ -341,15 +440,44 @@ if(USE_MMAP)
add_definitions(-DUSE_MMAP)
endif()
clear_wasm_sysroot()
find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
restore_wasm_sysroot()
message(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}")
message(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
if(NOT MINIMAL_BUILD)
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
clear_wasm_sysroot()
if(IFCXML_SUPPORT)
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 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()
endif()
restore_wasm_sysroot()
endif()
if(IFCXML_SUPPORT)
@@ -358,18 +486,335 @@ if(IFCXML_SUPPORT)
endif()
if(BUILD_IFCGEOM)
if(MSVC AND NOT LibXml2_DIR)
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
endif()
# Open CASCADE
if(WITH_OPENCASCADE)
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)
if(COLLADA_SUPPORT)
find_package(OpenCOLLADA REQUIRED)
if(COLLADA_SUPPORT AND (NOT OPENCOLLADA_INCLUDE_DIR AND NOT OPENCOLLADA_LIBRARY_DIR))
# If package is found, automatically sets
# OPENCOLLADA_INCLUDE_DIRS and OPENCOLLADA_LIBRARIES (list of targets, not paths).
find_package(OpenCOLLADA CONFIG)
if(OpenCOLLADA_DIR)
message(STATUS "Found OpenCOLLADA: '${OpenCOLLADA_DIR}'.")
set(OPENCOLLADA_FOUND TRUE)
else()
message(STATUS "OpenCOLLADA package not found, falling back to manual search.")
endif()
endif()
if(COLLADA_SUPPORT AND NOT OpenCOLLADA_DIR)
# Find OpenCOLLADA
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA include directory specified")
set(OPENCOLLADA_INCLUDE_DIR "/usr/include/opencollada" CACHE FILEPATH "OpenCOLLADA header files")
else()
set(OPENCOLLADA_INCLUDE_DIR "${OPENCOLLADA_INCLUDE_DIR}" CACHE FILEPATH "OpenCOLLADA header files")
endif()
if("${OPENCOLLADA_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA library directory specified")
find_library(OPENCOLLADA_FRAMEWORK_LIB NAMES OpenCOLLADAFramework
PATHS /usr/lib64/opencollada /usr/lib/opencollada /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib)
get_filename_component(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_FRAMEWORK_LIB} PATH)
endif()
find_library(OpenCOLLADAFramework NAMES OpenCOLLADAFramework OpenCOLLADAFrameworkd PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
if(OpenCOLLADAFramework)
message(STATUS "OpenCOLLADA library files found")
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA libraries. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
set(OPENCOLLADA_LIBRARY_DIR "${OPENCOLLADA_LIBRARY_DIR}" CACHE FILEPATH "OpenCOLLADA library files")
set(OPENCOLLADA_INCLUDE_DIRS "${OPENCOLLADA_INCLUDE_DIR}/COLLADABaseUtils" "${OPENCOLLADA_INCLUDE_DIR}/COLLADAStreamWriter")
find_file(COLLADASWStreamWriter_h "COLLADASWStreamWriter.h" ${OPENCOLLADA_INCLUDE_DIRS})
if(COLLADASWStreamWriter_h)
message(STATUS "OpenCOLLADA header files found")
set(OPENCOLLADA_FOUND TRUE)
set(OPENCOLLADA_LIBRARY_NAMES
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
OpenCOLLADAStreamWriter UTF buffer ftoa
)
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
endforeach()
if("${PCRE_LIBRARY_DIR}" STREQUAL "")
if(WIN32)
find_library(pcre_library NAMES pcre pcred PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
else()
find_library(pcre_library NAMES pcre PATHS ${OPENCOLLADA_LIBRARY_DIR})
endif()
get_filename_component(PCRE_LIBRARY_DIR ${pcre_library} PATH)
else()
find_library(pcre_library NAMES pcre pcred PATHS ${PCRE_LIBRARY_DIR} NO_DEFAULT_PATH)
endif()
if(pcre_library)
set(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_LIBRARY_DIR} ${PCRE_LIBRARY_DIR})
if(MSVC)
# Add release lib regardless whether release or debug found. Debug version will be appended below.
list(APPEND OPENCOLLADA_LIBRARIES "${PCRE_LIBRARY_DIR}/pcre.lib")
else()
list(APPEND OPENCOLLADA_LIBRARIES "${pcre_library}")
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find PCRE. "
"Disable COLLADA_SUPPORT or fix PCRE_LIBRARY_DIR path to proceed.")
endif()
if(MSVC)
add_debug_variants(OPENCOLLADA_LIBRARIES "${OPENCOLLADA_LIBRARIES}" d)
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA headers. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
endif(COLLADA_SUPPORT AND NOT OpenCOLLADA_DIR)
if(COLLADA_SUPPORT AND OPENCOLLADA_FOUND)
add_definitions(-DWITH_OPENCOLLADA)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
endif()
if(HDF5_SUPPORT)
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)
@@ -486,12 +931,25 @@ else()
endif(MSVC)
find_package(Eigen3 REQUIRED)
link_libraries(Eigen3::Eigen)
# Ensure other dependencies are provided.
if(NOT EXISTS "${EIGEN_DIR}")
find_package(Eigen3 CONFIG)
if(Eigen3_DIR)
message(STATUS "Eigen3: found config at '${Eigen3_DIR}'.")
else()
message(
FATAL_ERROR
"EIGEN_DIR is not provided or provided folder doesn't exist (current value: '${EIGEN_DIR}'). "
"Also couldn't find Eigen3 as a package."
)
endif()
link_libraries(Eigen3::Eigen)
endif()
include_directories(${INCLUDE_DIRECTORIES} ${OPENCOLLADA_INCLUDE_DIRS}
include_directories(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS}
${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${HDF5_INCLUDE_DIR}
${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
${EIGEN_DIR} ${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
${TBB_INCLUDE_DIR}
)
@@ -580,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)
@@ -591,31 +1048,84 @@ 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} OBJECT ../src/ifcgeom/Serialization/schema/Serialization.cpp)
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()
add_library(geometry_serializer OBJECT ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
add_library(geometry_serializer STATIC ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
set_target_properties(geometry_serializer PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS")
target_link_libraries(geometry_serializer ${geometry_serializer_libraries} IfcParse)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} geometry_serializer ${geometry_serializer_libraries})
endif()
endif()
# IfcParse
file(GLOB IFCPARSE_H_FILES_ALL ../src/ifcparse/*.h)
file(GLOB IFCPARSE_CPP_FILES_ALL ../src/ifcparse/*.cpp)
foreach(file ${IFCPARSE_H_FILES_ALL})
get_filename_component(filename "${file}" NAME)
if(NOT "${filename}" MATCHES "[0-9]")
list(APPEND IFCPARSE_H_FILES "${file}")
endif()
endforeach()
foreach(file ${IFCPARSE_CPP_FILES_ALL})
get_filename_component(filename "${file}" NAME)
if(NOT "${filename}" MATCHES "[0-9]")
list(APPEND IFCPARSE_CPP_FILES "${file}")
endif()
endforeach()
foreach(schema ${SCHEMA_VERSIONS})
list(APPEND IFCPARSE_H_FILES
../src/ifcparse/Ifc${schema}.h
../src/ifcparse/Ifc${schema}-definitions.h
)
list(APPEND IFCPARSE_CPP_FILES
../src/ifcparse/Ifc${schema}.cpp
../src/ifcparse/Ifc${schema}-schema.cpp
)
endforeach()
set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
add_library(IfcParse ${IFCPARSE_FILES})
target_link_libraries(IfcParse ${STDCPPFS})
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
if(LibXml2_DIR)
target_compile_definitions(IfcParse PRIVATE ${LIBXML2_DEFINITIONS})
endif()
if(WASM_BUILD)
target_link_libraries(IfcParse ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
else()
target_link_libraries(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
endif()
if(BUILD_IFCGEOM)
# CGAL::CGAL target already has dependencies resolved.
if(WITH_CGAL AND CGAL_DIR)
set(CGAL_LIBRARIES CGAL::CGAL)
message(STATUS "Using found CGAL package at '${CGAL_DIR}'")
elseif(WITH_CGAL AND NOT CGAL_DIR)
clear_wasm_sysroot()
find_library(libGMP NAMES gmp mpir PATHS ${GMP_LIBRARY_DIR} NO_DEFAULT_PATH)
find_library(libMPFR NAMES mpfr PATHS ${MPFR_LIBRARY_DIR} NO_DEFAULT_PATH)
restore_wasm_sysroot()
if(NOT libGMP)
message(FATAL_ERROR "Unable to find GMP library files, aborting")
endif()
@@ -627,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)
@@ -636,9 +1171,12 @@ if(BUILD_IFCGEOM)
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/mapping/*.cpp)
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES} ${IFCGEOM_I_FILES})
add_library(geometry_mapping_ifc${schema} OBJECT ${IFCGEOM_FILES})
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
if (NOT BUILD_SHARED_LIBS)
target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
endif()
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
endforeach()
@@ -654,22 +1192,134 @@ if(BUILD_IFCGEOM)
find_package(Threads)
endif()
target_link_libraries(IfcGeom IfcParse ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT} ${OpenCASCADE_LIBRARIES})
target_link_libraries(IfcGeom IfcParse ${mapping_libraries} ${CMAKE_THREAD_LIBS_INIT})
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)
@@ -756,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
@@ -784,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": ""
}
-42
View File
@@ -1,42 +0,0 @@
#
# Input variables:
# - `CGAL_INCLUDE_DIR`
# - `CGAL_LIBRARY_DIR`
# - `GMP_INCLUDE_DIR`
# - `GMP_LIBRARY_DIR`
# - `MPFR_INCLUDE_DIR`
# - `MPFR_LIBRARY_DIR`
# If input variables are not specified, try to find HDF5 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `CGAL_INCLUDE_DIR`
#
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
if(NOT CGAL_INCLUDE_DIR)
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
# and sometiems it's getting in the way.
if(NOT DEFINED Boost_USE_STATIC_LIBS)
set(CGAL_Boost_USE_STATIC_LIBS OFF)
else()
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
endif()
find_package(CGAL CONFIG REQUIRED)
if(NOT CGAL_DIR)
message(
FATAL_ERROR
"CGAL_SUPPORT enabled, but CGAL_INCLUDE_DIR wasn't provided and CGAL package couldn't be found."
)
endif()
message(STATUS "CGAL: found config at '${CGAL_DIR}'.")
endif()
add_definitions(-DIFOPSH_WITH_CGAL)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
-27
View File
@@ -1,27 +0,0 @@
#
# Input variables:
# - `EIGEN_DIR`
# If input variables are not specified, try to find Eigen3 config.
# Input variables could also be provided as environment variables.
#
# Output targets:
# - `Eigen3::Eigen`
UNIFY_ENVVARS_AND_CACHE(EIGEN_DIR)
if(EXISTS "${EIGEN_DIR}")
# Mimic Eigen3Config.cmake target.
add_library(Eigen3::Eigen INTERFACE IMPORTED)
target_include_directories(Eigen3::Eigen INTERFACE "${EIGEN_DIR}")
else()
find_package(Eigen3 CONFIG)
if(Eigen3_DIR)
message(STATUS "Eigen3: found config at '${Eigen3_DIR}'.")
else()
message(
FATAL_ERROR
"EIGEN_DIR is not provided or provided folder doesn't exist (current value: '${EIGEN_DIR}'). "
"Also couldn't find Eigen3 as a package."
)
endif()
endif()
-109
View File
@@ -1,109 +0,0 @@
#
# Input variables:
# - `HDF5_INCLUDE_DIR`
# - `HDF5_LIBRARY_DIR`
# - `HDF5_LIBRARIES`
# If input variables are not specified, try to find HDF5 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `HDF5_INCLUDE_DIR`
# - `HDF5_LIBRARY_DIR`
# - `HDF5_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
# To avoid cyclic calls to this file
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No HDF5 include directory specified")
else()
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
endif()
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No HDF5 library directory specified")
else()
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
endif()
if(HDF5_LIBRARY_DIR)
# result of the HDF5 ctest package
# Find zlib using cmake find_library. How should this be implemented?
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
if("$ENV{CONDA_BUILD}" STREQUAL "")
# result of the HDF5 ctest package
if(WIN32)
set(zlib_post lib)
set(lib_ext lib)
else()
set(lib_ext a)
endif()
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(debug_postfix "_debug")
endif()
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
)
else()
message(STATUS "Packaging hdf5 and zlib for conda distribution")
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
# macOS
set(zlib_post libz)
set(lib_ext dylib)
set(HDF5_LIBRARIES
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
)
else()
# linux and windows
# Find HDF5 package
find_package(HDF5 REQUIRED COMPONENTS C CXX)
# Find ZLIB package
find_package(ZLIB REQUIRED)
# Include directories
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
# Link libraries
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
endif()
endif()
endif()
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
# First try to find it as a config.
find_package(HDF5 CONFIG)
if(HDF5_DIR)
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
set(HDF5_LIBRARIES hdf5_cpp-static)
else()
# If it failed, still try to find as a module.
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
find_package(HDF5 COMPONENTS CXX)
if(NOT HDF5_INCLUDE_DIR)
message(
FATAL_ERROR
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
"Also could not find HDF5 package (neither module or config)."
)
endif()
endif()
endif()
# Restore module path.
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
-49
View File
@@ -1,49 +0,0 @@
#
# Input variables:
# - `LIBXML2_INCLUDE_DIR`
# - `LIBXML2_LIBRARIES`
# If input variables are not specified, try to find LibXml2 config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `LIBXML2_INCLUDE_DIR`
# - `LIBXML2_LIBRARIES`
#
# To avoid cyclic calls to this file
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
# CONFIG is provided using root path, so no need to clear sysroot here.
find_package(LibXml2 QUIET CONFIG)
if(NOT LibXml2_FOUND)
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
find_package(LibXml2 REQUIRED)
else()
message(STATUS "Found LibXml2 config: ${LibXml2_DIR}")
endif()
if(TARGET LibXml2::LibXml2)
# Config mode already gives us the target
set(LIBXML2_LIBRARIES LibXml2::LibXml2)
get_target_property(LIBXML2_INCLUDE_DIR LibXml2::LibXml2 INTERFACE_INCLUDE_DIRECTORIES)
else()
# Module mode (Ubuntu)
set(LIBXML2_LIBRARIES ${LibXml2_LIBRARIES})
set(LIBXML2_INCLUDE_DIR ${LibXml2_INCLUDE_DIRS})
endif()
else()
find_package(LibXml2 REQUIRED)
endif()
if(MSVC AND NOT LibXml2_DIR)
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
endif()
# Restore module path.
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
-144
View File
@@ -1,144 +0,0 @@
#
# Input variables:
# - `OCC_INCLUDE_DIR`
# - `OCC_LIBRARY_DIR`
# If input variables are not specified, try to find OpenCASCADE config.
# Input variables could also be provided as environment variables.
#
# Output variables
# - `OpenCASCADE_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
if(OCC_INCLUDE_DIR)
set(OCC_INCLUDE_DIR ${OCC_INCLUDE_DIR} CACHE FILEPATH "Open CASCADE header files")
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
endif()
if(OCC_LIBRARY_DIR)
set(OCC_LIBRARY_DIR ${OCC_LIBRARY_DIR} CACHE FILEPATH "Open CASCADE library files")
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
endif()
if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
# OCE is not supported for find_package, because it's using a different name (`oce`)
# and also has an odd directory structure (install/lib/oce-0.18/*.cmake).
# find_package creates variables:
# - `OpenCASCADE_INCLUDE_DIR`
# - `OpenCASCADE_LIBRARIES`
# OpenCASCADE may be built with VTK support. Try to find VTK first to avoid
# CMake errors when OpenCASCADE's config references VTK targets.
find_package(VTK QUIET)
find_package(OpenCASCADE CONFIG REQUIRED)
message(STATUS "Found OpenCASCADE config: ${OpenCASCADE_DIR}")
return()
endif()
# No specific paths specified, try to find package.
if(OCC_INCLUDE_DIR AND OCC_LIBRARY_DIR)
message(
STATUS
"Using provided OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') "
"and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}')."
)
# Parse OCC_VERSION_STRING.
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
REGEX "#define OCC_VERSION_MAJOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
REGEX "#define OCC_VERSION_MINOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
REGEX "#define OCC_VERSION_MAINTENANCE.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
else()
message(
FATAL_ERROR
"Couldn't find Open CASCADE installation. "
"Either both OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}') "
"must be specified or OpenCASCADE package should be discoverable. "
"If you're using OCE, then providing a package is not available "
"and you need to provide OCE_INCLUDE_DIR and OCE_LIBRARY_DIR directly."
)
endif()
set(
OpenCASCADE_LIBRARIES
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
TKFillet TKXSBase TKOffset TKHLR
# @todo investigate the exact conditions when this is necessary
TKBin
)
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OpenCASCADE_LIBRARIES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OpenCASCADE_LIBRARIES TKDESTEP TKDEIGES)
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
if(libTKernel)
message(STATUS "Required Open Cascade Library files found")
else()
message(
FATAL_ERROR
"Unable to find Open Cascade library files in OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}'), aborting"
)
endif()
if(MSVC)
add_definitions(-DHAVE_NO_DLL)
add_debug_variants(OpenCASCADE_LIBRARIES "${OpenCASCADE_LIBRARIES}" d)
endif()
if(WIN32)
# OCC might require linking to Winsock depending on the version and build configuration
list(APPEND OpenCASCADE_LIBRARIES ws2_32.lib)
endif()
# Make sure cross-referenced symbols between static OCC libraries get
# resolved. Also add thread and rt libraries.
get_filename_component(libTKernelExt ${libTKernel} EXT)
if("${libTKernelExt}" STREQUAL ".a")
set(OCCT_STATIC ON)
endif()
if(OCCT_STATIC)
find_package(Threads)
if(WASM_BUILD)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
# OpenCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
if(APPLE)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
set(OpenCASCADE_LIBRARIES -Wl,--start-group ${OpenCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
endif()
endif()
if(NOT APPLE AND NOT WIN32)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "rt")
endif()
if(NOT WIN32)
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "dl")
endif()
endif()
add_library(OpenCASCADE_INTERFACE INTERFACE IMPORTED)
target_include_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_INCLUDE_DIR}")
target_link_libraries(OpenCASCADE_INTERFACE INTERFACE ${OpenCASCADE_LIBRARIES})
target_link_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_LIBRARY_DIR}")
set(OpenCASCADE_LIBRARIES OpenCASCADE_INTERFACE)
-118
View File
@@ -1,118 +0,0 @@
#
# Input variables:
# - `OPENCOLLADA_INCLUDE_DIR`
# - `OPENCOLLADA_LIBRARY_DIR`
# - `PCRE_LIBRARY_DIR`
# If input variables are not specified, try to find OpenCOLLADA config.
# Input variables could also be provided as environment variables.
#
# Output variables:
# - `OPENCOLLADA_INCLUDE_DIR`
# - `OPENCOLLADA_LIBRARY_DIR`
# - `OPENCOLLADA_LIBRARIES`
#
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
if(NOT OPENCOLLADA_INCLUDE_DIR AND NOT OPENCOLLADA_LIBRARY_DIR)
# If package is found, automatically sets
# OPENCOLLADA_INCLUDE_DIRS and OPENCOLLADA_LIBRARIES (list of targets, not paths).
find_package(OpenCOLLADA CONFIG)
if(OpenCOLLADA_DIR)
message(STATUS "Found OpenCOLLADA: '${OpenCOLLADA_DIR}'.")
set(OPENCOLLADA_FOUND TRUE)
else()
message(STATUS "OpenCOLLADA package not found, falling back to manual search.")
endif()
endif()
if(NOT OpenCOLLADA_DIR)
# Find OpenCOLLADA
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA include directory specified")
set(OPENCOLLADA_INCLUDE_DIR "/usr/include/opencollada" CACHE FILEPATH "OpenCOLLADA header files")
else()
set(OPENCOLLADA_INCLUDE_DIR "${OPENCOLLADA_INCLUDE_DIR}" CACHE FILEPATH "OpenCOLLADA header files")
endif()
if("${OPENCOLLADA_LIBRARY_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA library directory specified")
find_library(OPENCOLLADA_FRAMEWORK_LIB NAMES OpenCOLLADAFramework
PATHS /usr/lib64/opencollada /usr/lib/opencollada /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib)
get_filename_component(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_FRAMEWORK_LIB} PATH)
endif()
find_library(OpenCOLLADAFramework NAMES OpenCOLLADAFramework OpenCOLLADAFrameworkd PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
if(OpenCOLLADAFramework)
message(STATUS "OpenCOLLADA library files found")
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA libraries. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
set(OPENCOLLADA_LIBRARY_DIR "${OPENCOLLADA_LIBRARY_DIR}" CACHE FILEPATH "OpenCOLLADA library files")
set(OPENCOLLADA_INCLUDE_DIRS "${OPENCOLLADA_INCLUDE_DIR}/COLLADABaseUtils" "${OPENCOLLADA_INCLUDE_DIR}/COLLADAStreamWriter")
find_file(COLLADASWStreamWriter_h "COLLADASWStreamWriter.h" ${OPENCOLLADA_INCLUDE_DIRS})
if(COLLADASWStreamWriter_h)
message(STATUS "OpenCOLLADA header files found")
set(OPENCOLLADA_FOUND TRUE)
set(OPENCOLLADA_LIBRARY_NAMES
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
OpenCOLLADAStreamWriter UTF buffer ftoa
)
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
endforeach()
if("${PCRE_LIBRARY_DIR}" STREQUAL "")
if(WIN32)
find_library(pcre_library NAMES pcre pcred PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
else()
find_library(pcre_library NAMES pcre PATHS ${OPENCOLLADA_LIBRARY_DIR})
endif()
get_filename_component(PCRE_LIBRARY_DIR ${pcre_library} PATH)
else()
find_library(pcre_library NAMES pcre pcred PATHS ${PCRE_LIBRARY_DIR} NO_DEFAULT_PATH)
endif()
if(pcre_library)
set(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_LIBRARY_DIR} ${PCRE_LIBRARY_DIR})
if(MSVC)
# Add release lib regardless whether release or debug found. Debug version will be appended below.
list(APPEND OPENCOLLADA_LIBRARIES "${PCRE_LIBRARY_DIR}/pcre.lib")
else()
list(APPEND OPENCOLLADA_LIBRARIES "${pcre_library}")
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find PCRE. "
"Disable COLLADA_SUPPORT or fix PCRE_LIBRARY_DIR path to proceed.")
endif()
if(MSVC)
add_debug_variants(OPENCOLLADA_LIBRARIES "${OPENCOLLADA_LIBRARIES}" d)
endif()
else()
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA headers. "
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
endif()
endif(NOT OpenCOLLADA_DIR)
if(OPENCOLLADA_FOUND)
add_definitions(-DWITH_OPENCOLLADA)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
endif()
-54
View File
@@ -1,54 +0,0 @@
#
# Input variables:
# - `USD_INCLUDE_DIR`
# - `USD_LIBRARY_DIR`
# Input variables could also be provided as environment variables.
# TODO: Try to find USD config if varibales are not provided.
# TODO: does usd have a config file?
#
# Output variables:
# - `USD_LIBRARIES`
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
if("${USD_INCLUDE_DIR}" STREQUAL "")
find_path(USD_INCLUDE_DIR pxr.h
PATHS
/usr/include/pxr
/usr/local/include/pxr
REQUIRED
)
if(USD_INCLUDE_DIR)
message(STATUS "Found USD include files in: ${USD_INCLUDE_DIR}")
else()
message(FATAL_ERROR "Unable to find USD include directory, specify USD_INCLUDE_DIR manually.")
endif()
else()
set(USD_INCLUDE_DIR ${USD_INCLUDE_DIR} CACHE FILEPATH "USD header files")
message(STATUS "Looking for USD include files in: ${USD_INCLUDE_DIR}")
endif()
set(USD_LIBRARIES
usd_usd
usd_usdGeom
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_gf
)
find_library(USD_LIBRARY
NAMES ${USD_LIBRARIES}
PATHS ${USD_LIBRARY_DIR})
if(USD_LIBRARY)
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
link_directories(${USD_LIBRARY_DIR})
else()
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
endif()
add_definitions(-DWITH_USD)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
+1
View File
@@ -42,6 +42,7 @@ cmake -G "Ninja" ^
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
-D CITYJSON_SUPPORT:BOOL=OFF ^
../cmake
if errorlevel 1 exit 1
+1
View File
@@ -42,6 +42,7 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DBUILD_IFCGEOM:BOOL=ON \
-DBUILD_GEOMSERVER:BOOL=OFF \
-DBOOST_USE_STATIC_LIBS:BOOL=OFF \
-DCITYJSON_SUPPORT:BOOL=OFF \
./cmake
ninja
+9 -10
View File
@@ -243,9 +243,7 @@ if WASM:
# Pyodide still in transition from `FLAGS` to `FLAGS_INIT`.
# `FLAGS_INIT` allow us to provide flags using environment variables
# and providing `FLAGS` directly would break pyodide toolchain.
# NOTE: changes in pyodide-build currently got postponed:
# https://github.com/pyodide/pyodide-build/pull/249
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (99, 0, 0)
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.
@@ -943,9 +941,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",
@@ -1378,7 +1378,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}")
@@ -1431,9 +1430,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...")
@@ -1449,7 +1445,7 @@ 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:
@@ -1486,6 +1482,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.
@@ -1502,7 +1501,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.
+4 -1
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",
+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
+1 -20
View File
@@ -98,7 +98,7 @@ def initialize_bbim_semver():
def get_debug_info():
bbim_version = bbim_semver["version"]
debug_info = {
return {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
@@ -113,25 +113,6 @@ def get_debug_info():
"last_error": last_error,
}
# Add .blend file save information
if bpy.data.is_saved:
debug_info["blend_file_path"] = bpy.data.filepath
debug_info["blend_file_dirty"] = bpy.data.is_dirty
else:
debug_info["blend_file_path"] = "Not saved"
debug_info["blend_file_dirty"] = "N/A"
# Add IFC file information
bim_props = tool.Blender.get_bim_props()
if bim_props.ifc_file:
debug_info["ifc_file_path"] = bim_props.ifc_file
debug_info["ifc_is_dirty"] = bim_props.is_dirty
else:
debug_info["ifc_file_path"] = "No IFC loaded"
debug_info["ifc_is_dirty"] = "N/A"
return debug_info
def format_debug_info(info: dict):
last_actions = ""
+9 -47
View File
@@ -21,7 +21,7 @@ import bpy
import bpy.utils.previews
import importlib
from bpy_extras.io_utils import ImportHelper, ExportHelper
from . import handler, ui, prop, operator, gizmo
from . import handler, ui, prop, operator
from typing import Union
from collections.abc import Callable
@@ -148,11 +148,7 @@ classes = [
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.DocPreferences,
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
ui.GizmoPreferencesWindow, # Register before GizmoPreferences
ui.GizmoPreferencesStair, # Register before GizmoPreferences
ui.GizmoPreferences,
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
ui.BIM_ADDON_preferences,
# Tabs panel
ui.BIM_PT_tabs,
# Project overview
@@ -206,19 +202,6 @@ classes = [
ui.BIM_PT_section_with_cappings,
ui.BIM_PT_decorators_overlay,
ui.BIM_PT_snappping,
# Gizmos
gizmo.BIM_OT_gizmo_value_input,
gizmo.GizmoArrow,
gizmo.GizmoArrow2D,
gizmo.GizmoCone,
gizmo.GizmoLock,
gizmo.GizmoArc,
gizmo.GizmoPen,
gizmo.GizmoValidate,
gizmo.GizmoCancel,
gizmo.GizmoPlus,
gizmo.GizmoMinus,
gizmo.GizmoCycle,
]
for mod in modules.values():
@@ -242,33 +225,14 @@ def on_register(scene):
is_registering = False
# Classes that need to be registered after modules (due to cross-module dependencies)
late_classes = (
ui.DefaultParameters, # Register before BIM_ADDON_preferences
ui.BIM_ADDON_preferences,
)
def register_classes(classes_to_register):
for cls in classes_to_register:
def register():
for cls in classes:
# Prevent crashes in Blender 4.4.0, see #6420.
if issubclass(cls, (ImportHelper, ExportHelper)):
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
bpy.utils.register_class(cls)
def unregister_classes(classes_to_unregister):
for cls in reversed(classes_to_unregister):
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
bpy.utils.unregister_class(cls)
def register():
register_classes(classes)
bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_post.append(handler.redo_post)
@@ -292,9 +256,6 @@ def register():
for mod in modules.values():
mod.register()
# Delay registering classes that depend on module classes
register_classes(late_classes)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
@@ -326,7 +287,11 @@ def unregister():
bpy.utils.previews.remove(icons)
unregister_classes(classes)
for cls in reversed(classes):
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
@@ -341,9 +306,6 @@ def unregister():
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
bpy.types.STATUSBAR_HT_header.remove(ui.draw_statusbar)
# Unregister late classes before modules
unregister_classes(late_classes)
for mod in reversed(list(modules.values())):
mod.unregister()
@@ -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
File diff suppressed because it is too large Load Diff
+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
@@ -127,10 +126,6 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
# Second pass: delete aggregates that now have no parts
deleted_aggregates = []
for aggregate in aggregates_to_check:
# Skip spatial elements - they should not be deleted even if empty
if aggregate.is_a("IfcSpatialElement"):
continue
related_objects = ifcopenshell.util.element.get_parts(aggregate)
if len(related_objects) == 0:
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
@@ -326,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"}
+17
View File
@@ -109,6 +109,23 @@ class BIM_PT_debug(Panel):
row.prop(props, "display_type", text="")
row.operator("bim.override_display_type").display = props.display_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and (data := context.active_object.data):
mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
row = layout.row(align=True)
row.prop(ifc_parameter, "name", text="")
row.prop(ifc_parameter, "value", text="")
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
layout.label(text="Inspector:")
row = layout.row(align=True)
+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`
@@ -489,7 +499,7 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@cache
def format_value(self, context, value, suppress_zero_inches=False, custom_unit=None, in_unit_length=False):
def format_value(self, context, value, custom_unit=None):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
if not precision:
@@ -500,9 +510,8 @@ class BaseDecorator:
value,
precision=precision,
decimal_places=decimal_places,
suppress_zero_inches=suppress_zero_inches,
suppress_zero_inches=True,
custom_unit=custom_unit,
in_unit_length=in_unit_length,
)
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
@@ -744,12 +753,7 @@ class DimensionDecorator(BaseDecorator):
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(
context,
length,
suppress_zero_inches=dimension_data["suppress_zero_inches"],
custom_unit=dimension_data["custom_unit"],
)
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
if isinstance(self, DiameterDecorator):
text = "D" + text
text = text_prefix + text + text_suffix
@@ -1204,11 +1208,7 @@ class PlanLevelDecorator(BaseDecorator):
abs_z = verts[0].z
z = helper.get_relative_z(obj, element, abs_z)
# Z is in meters (Blender world space), convert to project units (feet)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
z = z / unit_scale # Convert meters to feet
rl = self.format_value(context, z, in_unit_length=True)
rl = self.format_value(context, z)
text = "{}{}".format("" if z < 0 else "+", rl)
return text
@@ -1281,11 +1281,7 @@ class SectionLevelDecorator(BaseDecorator):
def get_text():
z = verts[0].z
# Z is in meters (Blender world space), convert to project units (feet)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
z = z / unit_scale # Convert meters to feet
rl = self.format_value(context, z, in_unit_length=True)
rl = self.format_value(context, z)
text = "RL {}{}".format("" if z < 0 else "+", rl)
return text
@@ -1645,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)
@@ -1742,38 +1731,21 @@ 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
# Handle both old float64 and new float32 checksums for version compatibility
loc_checksum_bytes = eval(DecoratorData.camera_location_checksum)
if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64)
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32)
else: # New format: 3 * 4 bytes (float32)
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32)
loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten()
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
# Handle both old float64 and new float32 checksums for version compatibility
rot_checksum_bytes = eval(DecoratorData.camera_rotation_checksum)
if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64)
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3)
else: # New format: 9 * 4 bytes (float32)
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32)
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
rot_real = np.array(obj.matrix_world.to_3x3())
rot_dot = np.dot(rot_check, rot_real.T)
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
if angle_rad > 0.0017453292519943296: # 0.1 degrees
@@ -2002,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()
+32 -100
View File
@@ -18,7 +18,6 @@
import bpy
import math
import ifcopenshell.util.element
import mathutils.geometry
import ifcopenshell
import ifcopenshell.util.unit
@@ -122,45 +121,6 @@ class BoundingBox:
return retVal
def find_best_precision(fractional_inches, max_precision=256, base_tolerance=0.0001):
"""
Find the simplest fraction denominator that accurately represents the value.
Uses a sliding tolerance - smaller denominators get more tolerance.
:param fractional_inches: The fractional part of inches (0.0 to 1.0)
:param max_precision: Maximum denominator to try (256 for imperial)
:param base_tolerance: Base tolerance in inches (very strict)
:return: Best denominator (power of 2: 2, 4, 8, 16, 32, 64, 128, 256)
"""
if abs(fractional_inches) < 0.00001:
return 1 # No fraction needed
# Try denominators from coarsest to finest: 2, 4, 8, 16, 32, 64, 128, 256
for exp in range(1, 9): # 2^1 to 2^8
denom = 2**exp
if denom > max_precision:
break
# Find nearest numerator for this denominator
numer = round(fractional_inches * denom)
# Skip if numerator is 0
if numer == 0:
continue
# Check if this fraction is close enough
fraction_value = numer / denom
error = abs(fraction_value - fractional_inches)
# Very strict tolerance - only simplify if it's a near-perfect match
# This keeps 3/256 as 3/256, but allows floating point errors to be cleaned up
if error < base_tolerance:
return denom
# If no simpler fraction works, use max precision
return max_precision
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
# MeasureIt-ARCH is GPL-v3
def format_distance(
@@ -226,18 +186,14 @@ def format_distance(
if unit_length == "INCHES":
toInches = 1
if unit_length == "FEET":
toInches = 12
toInches = 11.9999
else:
toInches = 39.3700787401574887
inPerFoot = 12.0
inPerFoot = 11.9999
if isArea:
toInches = 1550
inPerFoot = 144
decInches = value * toInches
decFeet = decInches / 12
inPerFoot = 143.999
if not precision:
precision = 256
elif precision == "1":
@@ -246,6 +202,8 @@ def format_distance(
precision = int(precision.split("/")[1])
base = int(precision)
decInches = value * toInches
decFeet = decInches / 12
# Separate ft and inches
# Unless Inches are the specified Length Unit or unit_fraction is False
@@ -261,33 +219,20 @@ def format_distance(
# Separate Fractional Inches
decInches = abs(decInches) # ignore the sign for inches
inches = math.floor(decInches) # remove decimal
# Clean up floating point errors
fractional_inches = decInches - inches
tolerance = 0.01 # About 1/100 of an inch
# If the fractional part is very small, treat it as zero
if fractional_inches < tolerance:
frac = 0
# If very close to the next whole inch, round up
elif fractional_inches > (1.0 - tolerance):
frac = 0
inches += 1
if inches != 0:
frac = round(base * (decInches - inches))
else:
# Calculate fraction normally
if inches != 0:
frac = round(base * fractional_inches)
else:
frac = round(base * fractional_inches)
frac = round(base * (decInches))
# Set proper numerator and denominator
if frac != base:
# Simplify using GCD
from math import gcd
divisor = gcd(int(frac), int(base))
frac = int(frac / divisor)
base = int(base / divisor)
numcycles = int(math.log2(base))
for i in range(numcycles):
if frac % 2 == 0:
frac = int(frac / 2)
base = int(base / 2)
else:
break
else:
frac = 0
inches += 1
@@ -306,46 +251,31 @@ def format_distance(
frac = None
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
tx_dist = ""
if feet:
tx_dist += str(feet) + "'"
if not feet and not add_inches:
tx_dist += str(feet) + "'"
# Add "0' - " when we have inches but no feet
# But only add " - " separator if we actually have inches to show
if not feet and add_inches:
tx_dist += "0' - "
elif feet and add_inches:
if feet and add_inches:
tx_dist += " - "
if not feet and value < 0:
tx_dist += "-"
if add_inches:
if feet == 0 and inches == 0 and not frac:
# Special case: exactly zero, show "0"
tx_dist += "0"
elif feet == 0 and inches == 0:
pass # Has fraction, will be added below
if feet == 0 and inches == 0:
pass
else:
tx_dist += str(inches)
if add_inches and frac:
if feet == 0 and inches == 0:
pass # Has fraction, will be added below
pass
else:
tx_dist += " "
if frac:
tx_dist += str(frac) + "/" + str(base)
if add_inches or frac:
# Only add inch symbol if we actually added inch content
if inches > 0 or frac > 0 or feet == 0:
tx_dist += '"'
tx_dist += '"'
if precision == "12" and unit_system == "IMPERIAL":
tx_dist = str(round(decFeet)) + "'"
if tx_dist == '"':
tx_dist = "0' - 0\""
else:
fmt = "%1.3f"
sq_feet = round(value * toInches / inPerFoot, 4)
@@ -491,8 +421,8 @@ def ortho_view_frame(
Similar to `bpy.types.Camera.view_frame`
:param camera: camera of drawing
:param margin: margins, in scene units
:arg camera: camera of drawing
:arg margin: margins, in scene units
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
props = tool.Drawing.get_camera_props(camera)
@@ -513,8 +443,8 @@ def almost_zero(v):
def clip_segment(bounds, segm):
"""Clipping line segment to bounds
:param bounds: (xmin, xmax, ymin, ymax)
:param segm: 2 vertices of the segment
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
# LiangBarsky algorithm
@@ -563,8 +493,8 @@ def clip_segment(bounds, segm):
def elevate_segment(bounds, segm):
"""Elevate line xy-perpendicular segment vertically
:param bounds: (xmin, xmax, ymin, ymax)
:param segm: 2 vertices of the segment
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
_, _, ymin, ymax, zmin, _ = bounds
@@ -614,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 -438
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:
@@ -975,10 +964,6 @@ class OverrideDelete(bpy.types.Operator):
except:
continue
# Skip spatial elements - they should not be deleted even if empty
if aggregate.is_a("IfcSpatialElement"):
continue
related_objects = ifcopenshell.util.element.get_parts(aggregate)
if len(related_objects) == 0:
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
@@ -1045,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
@@ -1075,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:
@@ -1148,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):
@@ -1182,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)
@@ -1240,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"
@@ -1265,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)
@@ -1281,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)
@@ -1293,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
@@ -1314,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
@@ -1342,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"]}
@@ -1383,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):
@@ -1485,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."""
@@ -1531,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")
@@ -1540,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] = {
@@ -1555,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:
@@ -2409,14 +2346,6 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Geometry.get_geometry_props()
item = tool.Geometry.get_active_representation(obj)
assert item
# Fix vertex order for annotation items
rep_obj = props.representation_obj
if rep_obj:
element = tool.Ifc.get_entity(rep_obj)
if element and self._is_annotation_object(element):
tool.Geometry.ensure_annotation_vertex_order(obj)
if tool.Geometry.is_meshlike_item(item):
if tool.Geometry.is_geometric_data(obj.data) and (
item.is_a("IfcVertex") or item.is_a("IfcEdge") or obj.data.polygons
@@ -2556,289 +2485,6 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
props.mode = "EDIT"
props.is_changing_mode = False
def _is_annotation_object(self, element: ifcopenshell.entity_instance) -> bool:
"""Check if element is an annotation object that needs vertex order preservation."""
if not element.is_a("IfcAnnotation"):
return False
# Object types that have semantic vertex order (arrow direction, text position)
annotation_types_with_order = {
"TEXT_LEADER",
"DIMENSION",
"RADIUS",
"DIAMETER",
"ANGLE",
"FALL",
"SLOPE_ANGLE",
"SLOPE_FRACTION",
"SLOPE_PERCENT",
"STAIR_ARROW",
"PLAN_LEVEL",
"SECTION_LEVEL",
"SECTION",
"ELEVATION",
}
return element.ObjectType in annotation_types_with_order
class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.direct_profile_edit"
bl_label = "IFC Direct Profile Edit"
bl_description = "Directly enter profile/axis edit mode, or exit back to object mode"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Only allow in OBJECT or EDIT modes
if context.mode not in ("OBJECT", "EDIT_MESH", "EDIT_CURVE"):
return False
# Must have an active object
if not context.active_object:
return False
# In edit mode, we're good to go (we'll exit)
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
return True
# In object mode, check if it's an editable object type
obj = context.active_object
# Only mesh and curve objects are editable
if obj.type not in ("MESH", "CURVE"):
return False
return True
def _execute(self, context):
# Ensure we're only working with the active object
# Deselect all other objects if multiple are selected
active_obj = context.active_object
if active_obj and context.mode == "OBJECT":
if len(context.selected_objects) > 1:
self.report(
{"WARNING"},
"Currently Direct Profile Edit only works on one object at a time. Operating on active object only.",
)
bpy.ops.object.select_all(action="DESELECT")
active_obj.select_set(True)
context.view_layer.objects.active = active_obj
# If we're already in edit mode, exit to object mode and SAVE changes
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
return self.handle_exit_edit_mode(context)
# We're in object mode, try to enter profile/axis edit
return self.handle_enter_edit_mode(context)
def handle_exit_edit_mode(self, context):
"""Handle exiting from edit mode and saving changes."""
obj = context.active_object
if not obj:
return {"CANCELLED"}
# Check if we're editing a representation item
if tool.Geometry.is_representation_item(obj):
return self.exit_item_edit_mode(context, obj)
# Check if we're editing an element-level profile or axis
element = tool.Ifc.get_entity(obj)
if element and tool.Geometry.has_mesh_properties(obj.data):
return self.exit_element_edit_mode(context, obj, element)
# For other edit modes, use standard operator
return bpy.ops.bim.override_mode_set_object()
def exit_item_edit_mode(self, context, obj):
"""Exit from SweptAreaSolid item editing."""
try:
item = tool.Geometry.get_active_representation(obj)
if not item or not item.is_a("IfcSweptAreaSolid"):
return bpy.ops.bim.override_mode_set_object()
# Use the standard save workflow for items
from bonsai.bim.module.model.decorator import ProfileDecorator
ProfileDecorator.uninstall()
tool.Blender.toggle_edit_mode(context)
profile = tool.Model.export_profile(obj)
if not profile:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
tool.Blender.toggle_edit_mode(context)
ProfileDecorator.install(context)
return {"CANCELLED"}
old_profile = item.SweptArea
profile.ProfileName = old_profile.ProfileName
ifc_file = tool.Ifc.get()
for inverse in ifc_file.get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
ifcopenshell.util.element.remove_deep2(ifc_file, old_profile)
props = tool.Geometry.get_geometry_props()
if props.representation_obj:
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.disable_item_mode()
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to save item changes: {str(e)}")
return {"CANCELLED"}
def exit_element_edit_mode(self, context, obj, element):
"""Exit from element-level profile or axis editing."""
try:
mesh_props = tool.Geometry.get_mesh_props(obj.data)
# Check if we're editing axis
if mesh_props.subshape_type == "AXIS":
return bpy.ops.bim.edit_extrusion_axis()
# Otherwise, we're editing a profile
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion:
return bpy.ops.bim.edit_extrusion_profile()
except Exception as e:
self.report({"ERROR"}, f"Failed to save profile/axis changes: {str(e)}")
return {"CANCELLED"}
# Fallback to standard operator
return bpy.ops.bim.override_mode_set_object()
def handle_enter_edit_mode(self, context):
"""Handle entering edit mode from object mode."""
obj = context.active_object
if not obj:
return {"CANCELLED"}
# Check if object has mesh or curve data
if not hasattr(obj, "data") or not obj.data:
self.report({"INFO"}, "Object has no editable data")
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
self.report({"INFO"}, "Active object is not an IFC element")
return {"CANCELLED"}
# Check if this is a LAYER2 element (wall, railing, etc.)
try:
material_usage = tool.Model.get_usage_type(element)
except:
material_usage = None
if material_usage == "LAYER2":
self.report({"ERROR"}, "LAYER2 elements (walls, railings, etc.) cannot use profile editing.")
return {"CANCELLED"}
# Try to get representation
try:
representation = tool.Geometry.get_active_representation(obj)
except:
representation = None
if not representation:
self.report({"INFO"}, "Object has no active representation")
return {"CANCELLED"}
# Check for PROFILE usage (beams, columns, members) - should edit axis
if material_usage == "PROFILE":
try:
return bpy.ops.bim.enable_editing_extrusion_axis()
except Exception as e:
self.report({"ERROR"}, f"Failed to enable axis editing: {str(e)}")
return {"CANCELLED"}
# Check if this is an element with an extrusion profile (LAYER3, etc)
try:
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion and not tool.Geometry.is_representation_item(obj):
return bpy.ops.bim.enable_editing_extrusion_profile()
except Exception as e:
self.report({"ERROR"}, f"Failed to enable profile editing: {str(e)}")
return {"CANCELLED"}
# Otherwise, try to enter item mode and edit SweptAreaSolid
return self.enter_item_edit_mode(context, obj)
def enter_item_edit_mode(self, context, obj):
"""Enter item mode for SweptAreaSolid editing."""
try:
props = tool.Geometry.get_geometry_props()
if not props.representation_obj:
bpy.ops.bim.import_representation_items()
props = tool.Geometry.get_geometry_props()
# Find the SweptAreaSolid item
swept_solid_obj = None
for item_obj_data in props.item_objs:
item_obj = item_obj_data.obj
if not item_obj:
continue
try:
item = tool.Geometry.get_active_representation(item_obj)
if item and item.is_a("IfcSweptAreaSolid"):
swept_solid_obj = item_obj
break
except:
continue
if not swept_solid_obj:
self.report({"INFO"}, "No editable SweptAreaSolid item found")
tool.Geometry.disable_item_mode()
return {"CANCELLED"}
# Select and activate the swept solid object
bpy.ops.object.select_all(action="DESELECT")
swept_solid_obj.select_set(True)
context.view_layer.objects.active = swept_solid_obj
# Import the profile to prepare for editing
item = tool.Geometry.get_active_representation(swept_solid_obj)
if item and item.is_a("IfcSweptAreaSolid"):
profile = item.SweptArea
res = tool.Model.import_profile(profile, obj=swept_solid_obj)
if res is None:
self.report({"ERROR"}, "Couldn't import profile for editing")
tool.Geometry.disable_item_mode()
return {"CANCELLED"}
tool.Ifc.link(item, swept_solid_obj.data)
tool.Blender.toggle_edit_mode(context)
from bonsai.bim.module.model.decorator import ProfileDecorator
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Failed to enter item edit mode: {str(e)}")
try:
tool.Geometry.disable_item_mode()
except:
pass
return {"CANCELLED"}
return {"CANCELLED"}
class FlipObject(bpy.types.Operator):
bl_idname = "bim.flip_object"
@@ -3407,17 +3053,6 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
tool.Root.reload_item_decorator()
element = tool.Ifc.get_entity(obj)
if element and element.is_a("IfcAnnotation") and element.ObjectType in {
"TEXT_LEADER", "DIMENSION", "RADIUS", "DIAMETER", "ANGLE",
"FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT",
"STAIR_ARROW", "PLAN_LEVEL", "SECTION_LEVEL", "SECTION", "ELEVATION"
}:
for item_obj_data in props.item_objs:
item_obj = item_obj_data.obj
if item_obj and isinstance(item_obj.data, bpy.types.Mesh) and item_obj.data.vertices:
item_obj.data["bonsai_first_vert_co"] = item_obj.data.vertices[0].co[:]
class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_item_attributes"
+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)
@@ -161,10 +161,6 @@ classes = (
stair.FinishEditingStair,
stair.EnableEditingStair,
stair.RemoveStair,
stair.ToggleStairTotalLengthLock,
stair.AdjustStairTreads,
stair.CycleStairType,
stair.GizmoStairEdition,
sverchok_modifier.CreateNewSverchokGraph,
sverchok_modifier.UpdateDataFromSverchok,
sverchok_modifier.DeleteSverchokGraph,
@@ -176,17 +172,12 @@ classes = (
window.FinishEditingWindow,
window.EnableEditingWindow,
window.RemoveWindow,
window.CycleWindowType,
window.GizmoWindowEdition,
door.BIM_OT_add_door,
door.AddDoor,
door.CancelEditingDoor,
door.FinishEditingDoor,
door.EnableEditingDoor,
door.RemoveDoor,
door.ToggleDoorSwing,
door.CycleDoorType,
door.GizmoDoorEdition,
railing.BIM_OT_add_railing,
railing.CopyRailingParameters,
railing.AddRailing,
+9 -309
View File
@@ -33,15 +33,13 @@ import bonsai.core.geometry
import bonsai.core.geometry as core
import bonsai.core.root
from bonsai.bim.module.model.window import create_bm_window, create_bm_box
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from mathutils import Vector, Matrix
from typing import Union, Any, Optional
import json
import collections
import collections.abc
from typing import get_args
V_ = tool.Blender.V_
@@ -194,8 +192,8 @@ def bm_mirror(
def create_bm_extruded_profile(
bm: bmesh.types.BMesh,
points: list[Vector],
edges: list[tuple[int, int]] | None = None,
faces: list[list[int]] | None = None,
edges: Optional[list[tuple[int, int]]] = None,
faces: Optional[list[list[int]]] = None,
position: Vector = V_(0, 0, 0).freeze(),
magnitude: float = 1.0,
extrusion_vector: Vector = V_(0, 0, 1).freeze(),
@@ -280,7 +278,7 @@ def create_bm_door_lining(
new_faces = [bm.faces.new([new_verts[vi] for vi in face[1]]) for face in faces]
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
extrusion_vector = V_(0, 1, 0) * depth
extrusion_vector = Vector((0, 1, 0)) * depth
translate_verts = [v for v in extruded["geom"] if isinstance(v, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
@@ -293,8 +291,6 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
obj = context.active_object
assert obj
props = tool.Model.get_door_props(obj)
if not props.is_editing:
return
overall_width = props.overall_width
overall_height = props.overall_height
@@ -412,10 +408,7 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
if door_swing_type == "LEFT":
bm_mirror(
bm,
door_handle_verts,
mirror_axes=V_(1, 0, 0),
mirror_point=panel_position + V_(panel_size.x / 2, 0, 0),
bm, door_handle_verts, mirror_axes=V_(1, 0, 0), mirror_point=panel_position + V_(panel_size.x / 2, 0, 0)
)
door_handle_mirrored_verts = bm_mirror(
@@ -474,7 +467,6 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
lining_offset_verts = lining_verts + door_verts + window_lining_verts + frame_verts + glass_verts
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=lining_offset_verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if bpy.context.active_object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data)
@@ -486,14 +478,14 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_door"
bl_label = "Add Door"
bl_label = "Door"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context: bpy.types.Context) -> set[str]:
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a door.")
@@ -536,8 +528,6 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
assert element
props = tool.Model.get_door_props(obj)
tool.Blender.get_addon_preferences().default_parameters.door.copy_to(props)
door_data = props.get_general_kwargs(convert_to_project_units=True)
lining_props = props.get_lining_kwargs(convert_to_project_units=True)
panel_props = props.get_panel_kwargs(convert_to_project_units=True)
@@ -556,7 +546,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
)
update_door_modifier_representation(obj)
def _execute(self, context: bpy.types.Context) -> set[str]:
def _execute(self, context):
for obj in tool.Blender.get_selected_objects():
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
continue
@@ -567,7 +557,6 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_door"
bl_label = "Cancel Editing Door on Selected Objects"
bl_description = "Cancel editing and revert door parameters to their previous values"
bl_options = {"REGISTER", "UNDO"}
def cancel_editing_door_on_object(self, obj: bpy.types.Object) -> None:
@@ -602,7 +591,6 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_door"
bl_label = "Finish Editing Door on Selected Objects"
bl_description = "Apply changes and finish editing door parameters"
bl_options = {"REGISTER", "UNDO"}
def finish_editing_door_on_object(self, obj):
@@ -639,7 +627,6 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_door"
bl_label = "Enable Editing Door on Selected Objects"
bl_description = "Enter edit mode to modify door parameters interactively"
bl_options = {"REGISTER", "UNDO"}
def edit_door_on_obj(self, obj):
@@ -683,290 +670,3 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
for obj in tool.Blender.get_selected_objects():
self.remove_door_on_object(obj)
return {"FINISHED"}
class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
"""Toggle door swing direction and optionally flip door geometry.
Shift+Click (when flip_geometry=True): Flip geometry only without changing door direction"""
bl_idname = "bim.toggle_door_swing"
bl_label = "Toggle Door Swing"
bl_options = {"REGISTER", "UNDO"}
flip_geometry: bpy.props.BoolProperty(name="Flip Geometry", default=False)
flip_local_axes: bpy.props.EnumProperty(
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
)
skip_direction_change: bpy.props.BoolProperty(
name="Skip Direction Change", default=False, options={"HIDDEN", "SKIP_SAVE"}
)
def invoke(self, context, event):
self.skip_direction_change = event.shift
return self.execute(context)
def _toggle_swing_direction(self, obj: bpy.types.Object) -> bool:
"""Toggle door swing direction between LEFT and RIGHT."""
props = tool.Model.get_door_props(obj)
current_type = props.door_type
if "LEFT" in current_type:
props.door_type = current_type.replace("LEFT", "RIGHT")
return True
elif "RIGHT" in current_type:
props.door_type = current_type.replace("RIGHT", "LEFT")
return True
return False
def _execute(self, context):
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
return {"CANCELLED"}
is_door = tool.Blender.Modifier.is_door(element)
if self.flip_geometry:
tool.Geometry.flip_object(obj, self.flip_local_axes)
if not self.skip_direction_change and is_door:
self._toggle_swing_direction(obj)
elif is_door:
self._toggle_swing_direction(obj)
else:
return {"CANCELLED"}
return {"FINISHED"}
class CycleDoorType(bpy.types.Operator, tool.Ifc.Operator):
"""Cycle through available door types. Shift+click to cycle in reverse."""
bl_idname = "bim.cycle_door_type"
bl_label = "Cycle Door Type"
bl_options = {"REGISTER", "UNDO"}
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
def invoke(self, context, event):
self.reverse = event.shift
return self.execute(context)
def _execute(self, context):
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element or not tool.Blender.Modifier.is_door(element):
return {"CANCELLED"}
props = tool.Model.get_door_props(obj)
door_types = get_args(tool.Model.DoorType)
current_index = door_types.index(props.door_type) if props.door_type in door_types else 0
direction = -1 if self.reverse else 1
next_index = (current_index + direction) % len(door_types)
props.door_type = door_types[next_index]
return {"FINISHED"}
class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_door_edition"
bl_label = "Door Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
enable_editing_operator = "bim.enable_editing_door"
finish_editing_operator = "bim.finish_editing_door"
cancel_editing_operator = "bim.cancel_editing_door"
cycle_type_operator = "bim.cycle_door_type"
gizmo_props = [
GizmoPropConfig("overall_height", (0, 0, 1)),
GizmoPropConfig("overall_width", (1, 0, 0)),
GizmoPropConfig("threshold_thickness", (0, 0, 1)),
GizmoPropConfig("threshold_depth", (0, 1, 0)),
GizmoPropConfig("lining_depth", (0, 1, 0)),
GizmoPropConfig("lining_thickness", (-1, 0, 0)),
GizmoPropConfig("transom_offset", (0, 0, 1)),
GizmoPropConfig("transom_thickness", (0, 0, 1)),
GizmoPropConfig("casing_thickness", (-1, 0, 0)),
GizmoPropConfig("casing_depth", (0, 1, 0)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_door(element)
def get_props(self, obj):
return tool.Model.get_door_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.door
def should_hide_gizmo(self, attr_name, props):
"""Door-specific visibility rules for gizmos."""
if not props.is_editing:
return True
if attr_name == "threshold_depth" and props.threshold_thickness == 0.0:
return True
if attr_name == "transom_offset" and props.transom_thickness == 0.0:
return True
if attr_name == "casing_thickness" and props.lining_offset != 0.0:
return True
if attr_name == "casing_depth" and (props.lining_offset != 0.0 or props.casing_thickness == 0.0):
return True
return False
def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_overall_width(self, props):
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_threshold_thickness(self, props):
translation = Matrix.Translation(
V_(
props.overall_width / 2,
props.threshold_offset + props.threshold_depth / 2 + props.lining_offset,
props.threshold_thickness,
)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_threshold_depth(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.threshold_depth + props.lining_offset, props.threshold_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_lining_depth(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_lining_thickness(self, props):
translation = Matrix.Translation(
V_(props.overall_width - props.lining_thickness, props.lining_depth / 2, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_transom_offset(self, props):
# Position at the bottom of the transom (transom extends upward from offset)
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.transom_offset)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props):
# Position at the top of the transom (offset + thickness)
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.transom_offset + props.transom_thickness)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
@staticmethod
def _get_casing_gizmo_base_position(props) -> tuple[float, float, float]:
"""Get common base position for casing gizmos."""
x = -props.casing_thickness + props.lining_thickness
y_base = props.lining_depth + props.lining_offset
z = props.overall_height / 2
return x, y_base, z
def get_gizmo_matrix_casing_thickness(self, props):
x, y_base, z = self._get_casing_gizmo_base_position(props)
translation = Matrix.Translation(V_(x, y_base + props.casing_depth / 2, z))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_casing_depth(self, props):
x, y_base, z = self._get_casing_gizmo_base_position(props)
translation = Matrix.Translation(V_(x, y_base + props.casing_depth, z))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def setup(self, context):
# Use base class methods for common gizmos
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
# Door-specific: swing arc gizmos
prefs = tool.Blender.get_addon_preferences()
highlight_color = prefs.decorator_color_selected[:3]
inactive_color = prefs.decorator_color_background[:3]
special_color = prefs.decorator_color_special[:3]
self.gizmo_door_type = self.gizmos.new("VIEW3D_GT_arc")
self.gizmo_door_type.use_draw_scale = False
self.gizmo_door_type.color = special_color
self.gizmo_door_type.alpha = 0.5
self.gizmo_door_type.color_highlight = highlight_color
self.gizmo_door_type.prop_path = "BIMDoorProperties.door_type"
op = self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
op.flip_geometry = False
# Flip arc gizmo - mirrors the swing arc along X axis, flips door and toggles direction when clicked
self.gizmo_flip_arc = self.gizmos.new("VIEW3D_GT_arc")
self.gizmo_flip_arc.use_draw_scale = False
self.gizmo_flip_arc.color = inactive_color
self.gizmo_flip_arc.alpha = 0.5
self.gizmo_flip_arc.color_highlight = highlight_color
self.gizmo_flip_arc.prop_path = "BIMDoorProperties.door_type"
op = self.gizmo_flip_arc.target_set_operator("bim.toggle_door_swing")
op.flip_geometry = True
op.flip_local_axes = "XY"
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
self.update_property_gizmos(mw, props)
self.update_swing_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
def update_swing_gizmos(self, mw, props):
"""Update swing gizmo position and color based on editing state."""
prefs = tool.Blender.get_addon_preferences()
door_gizmo_prefs = prefs.gizmos.door
self.gizmo_door_type.hide = not props.is_editing or not door_gizmo_prefs.swing_arc
self.gizmo_flip_arc.hide = not props.is_editing or not door_gizmo_prefs.flip_arc
# If both are hidden, no need to calculate transforms
if self.gizmo_door_type.hide and self.gizmo_flip_arc.hide:
return
# Calculate base swing transformation (common to both arcs)
swing_x_offset = props.overall_width if "RIGHT" in props.door_type else 0.0
base_swing_transform = Matrix.Translation(V_(swing_x_offset, props.lining_offset, 0)) @ Matrix.Scale(
props.overall_width, 4
)
if not self.gizmo_door_type.hide:
self.gizmo_door_type.matrix_basis = mw @ base_swing_transform
self.gizmo_door_type.color = prefs.decorations_colour[:3]
if not self.gizmo_flip_arc.hide:
# Mirror the flip arc along Y axis to show the opposite swing direction
mirror_y = Matrix.Scale(-1, 4, (0, 1, 0))
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_y
+53 -241
View File
@@ -217,12 +217,7 @@ class BIMModelProperties(PropertyGroup):
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
x_angle: bpy.props.FloatProperty(
name="X Angle",
default=0,
subtype="ANGLE",
min=math.radians(-180),
max=math.radians(180),
update=update_x_angle,
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
@@ -353,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):
@@ -525,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,
)
@@ -542,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",
@@ -610,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):
@@ -633,32 +513,18 @@ 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])
def copy_to(self, target_props: "BIMStairProperties") -> None:
"""Copy preset values to target stair properties."""
target_props.custom_tread_lock = self.custom_tread_lock
for prop_name, prop_value in self.get_props_kwargs().items():
# Skip custom_first_last_tread_run if it contains None values (when lock is enabled)
if prop_name == "custom_first_last_tread_run" and None in prop_value:
continue
setattr(target_props, prop_name, prop_value)
class BIMSverchokProperties(PropertyGroup):
node_group: bpy.props.PointerProperty(name="Node Group", type=NodeTree)
@@ -671,6 +537,19 @@ def window_type_prop_update(self, context):
update_window(self, context)
WindowType = Literal[
"SINGLE_PANEL",
"DOUBLE_PANEL_HORIZONTAL",
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_VERTICAL",
]
# default prop values are in mm and converted later
class BIMWindowProperties(PropertyGroup):
non_si_units_props = (
@@ -699,7 +578,7 @@ class BIMWindowProperties(PropertyGroup):
is_editing: bpy.props.BoolProperty(default=False)
window_type: bpy.props.EnumProperty(
name="Window Type",
items=[(i, i, "") for i in get_args(tool.Model.WindowType)],
items=[(i, i, "") for i in get_args(WindowType)],
default="SINGLE_PANEL",
update=window_type_prop_update,
)
@@ -709,11 +588,9 @@ class BIMWindowProperties(PropertyGroup):
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6, subtype="DISTANCE", update=update_window)
# lining properties
lining_depth: bpy.props.FloatProperty(
name="Lining Depth", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
)
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_window)
lining_thickness: bpy.props.FloatProperty(
name="Lining Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_window
)
lining_offset: bpy.props.FloatProperty(
name="Lining Offset", default=0.050, subtype="DISTANCE", update=update_window
@@ -742,13 +619,12 @@ class BIMWindowProperties(PropertyGroup):
update=update_window,
)
transom_thickness: bpy.props.FloatProperty(
name="Transom Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
name="Transom Thickness", default=0.050, subtype="DISTANCE", update=update_window
)
first_transom_offset: bpy.props.FloatProperty(
name="First Transom Offset",
description="Distance from the first lining to the first transom center",
default=0.3,
min=0.001,
subtype="DISTANCE",
update=update_window,
)
@@ -775,7 +651,7 @@ class BIMWindowProperties(PropertyGroup):
if TYPE_CHECKING:
is_editing: bool
window_type: tool.Model.WindowType
window_type: WindowType
overall_height: float
overall_width: float
lining_depth: float
@@ -809,7 +685,7 @@ class BIMWindowProperties(PropertyGroup):
return tool.Model.convert_data_to_project_units(kwargs, ["window_type"])
def get_lining_kwargs(
self, window_type: Optional[tool.Model.WindowType] = None, convert_to_project_units: bool = False
self, window_type: Optional[WindowType] = None, convert_to_project_units: bool = False
) -> dict[str, Any]:
if not window_type:
window_type = self.window_type
@@ -867,13 +743,17 @@ class BIMWindowProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMWindowProperties") -> None:
"""Copy preset values to target window properties, excluding materials."""
exclude_props = {"lining_material", "framing_material", "glazing_material"}
for kwargs_dict in (self.get_general_kwargs(), self.get_lining_kwargs(), self.get_panel_kwargs()):
for prop_name, prop_value in kwargs_dict.items():
if prop_name not in exclude_props:
setattr(target_props, prop_name, prop_value)
DoorType = Literal[
"SINGLE_SWING_LEFT",
"SINGLE_SWING_RIGHT",
"DOUBLE_SWING_LEFT",
"DOUBLE_SWING_RIGHT",
"DOUBLE_DOOR_SINGLE_SWING",
"SLIDING_TO_LEFT",
"SLIDING_TO_RIGHT",
"DOUBLE_DOOR_SLIDING",
]
class BIMDoorProperties(PropertyGroup):
@@ -888,39 +768,17 @@ class BIMDoorProperties(PropertyGroup):
is_editing: bpy.props.BoolProperty(default=False)
door_type: bpy.props.EnumProperty(
name="Door Operation Type",
items=tuple((i, i, "") for i in get_args(tool.Model.DoorType)),
items=tuple((i, i, "") for i in get_args(DoorType)),
default="SINGLE_SWING_LEFT",
update=update_door,
)
overall_height: bpy.props.FloatProperty(
name="Overall Height",
default=2.0,
min=0,
subtype="DISTANCE",
update=update_door,
)
overall_width: bpy.props.FloatProperty(
name="Overall Width",
default=0.9,
min=0,
subtype="DISTANCE",
update=update_door,
)
overall_height: bpy.props.FloatProperty(name="Overall Height", default=2.0, subtype="DISTANCE", update=update_door)
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.9, subtype="DISTANCE", update=update_door)
# lining properties
lining_depth: bpy.props.FloatProperty(
name="Lining Depth",
default=0.050,
min=0.001,
subtype="DISTANCE",
update=update_door,
)
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_door)
lining_thickness: bpy.props.FloatProperty(
name="Lining Thickness",
default=0.050,
min=0.001,
subtype="DISTANCE",
update=update_door,
name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_door
)
lining_offset: bpy.props.FloatProperty(
name="Lining Offset",
@@ -942,7 +800,6 @@ class BIMDoorProperties(PropertyGroup):
name="Transom Thickness",
description="Set values > 0 to add a transom.\n" "`0.050 mm` is good as default value",
default=0.000,
min=0,
subtype="DISTANCE",
update=update_door,
)
@@ -950,7 +807,6 @@ class BIMDoorProperties(PropertyGroup):
name="Transom Offset",
description="Distance from the bottom door opening to the beginning of the transom (unlike windows)",
default=1.525,
min=0,
subtype="DISTANCE",
update=update_door,
)
@@ -959,32 +815,20 @@ class BIMDoorProperties(PropertyGroup):
name="Casing Thickness",
description="Set values > 0 and LiningOffset = 0 to add a casing.",
default=0.075,
min=0,
subtype="DISTANCE",
update=update_door,
)
casing_depth: bpy.props.FloatProperty(
name="Casing Depth",
default=0.005,
min=0.001,
subtype="DISTANCE",
update=update_door,
)
casing_depth: bpy.props.FloatProperty(name="Casing Depth", default=0.005, subtype="DISTANCE", update=update_door)
threshold_thickness: bpy.props.FloatProperty(
name="Threshold Thickness",
description="Set values > 0 to add a threshold.",
default=0.025,
min=0,
subtype="DISTANCE",
update=update_door,
)
threshold_depth: bpy.props.FloatProperty(
name="Threshold Depth",
default=0.1,
min=0.001,
subtype="DISTANCE",
update=update_door,
name="Threshold Depth", default=0.1, subtype="DISTANCE", update=update_door
)
threshold_offset: bpy.props.FloatProperty(
name="Threshold Offset",
@@ -995,13 +839,7 @@ class BIMDoorProperties(PropertyGroup):
)
# panel properties
panel_depth: bpy.props.FloatProperty(
name="Panel Depth",
default=0.035,
min=0,
subtype="DISTANCE",
update=update_door,
)
panel_depth: bpy.props.FloatProperty(name="Panel Depth", default=0.035, subtype="DISTANCE", update=update_door)
panel_width_ratio: bpy.props.FloatProperty(
name="Panel Width Ratio",
description="Width of this panel, given as ratio " "relative to the total clear opening width of the door",
@@ -1011,18 +849,10 @@ class BIMDoorProperties(PropertyGroup):
update=update_door,
)
frame_thickness: bpy.props.FloatProperty(
name="Window Frame Thickness",
default=0.035,
min=0,
subtype="DISTANCE",
update=update_door,
name="Window Frame Thickness", default=0.035, subtype="DISTANCE", update=update_door
)
frame_depth: bpy.props.FloatProperty(
name="Window Frame Depth",
default=0.035,
min=0,
subtype="DISTANCE",
update=update_door,
name="Window Frame Depth", default=0.035, subtype="DISTANCE", update=update_door
)
# Material properties
@@ -1032,7 +862,7 @@ class BIMDoorProperties(PropertyGroup):
if TYPE_CHECKING:
is_editing: bool
door_type: tool.Model.DoorType
door_type: DoorType
overall_height: float
overall_width: float
@@ -1130,14 +960,6 @@ class BIMDoorProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMDoorProperties") -> None:
"""Copy properties to target door properties, excluding materials."""
exclude_props = {"lining_material", "framing_material", "glazing_material"}
for kwargs_dict in (self.get_general_kwargs(), self.get_lining_kwargs(), self.get_panel_kwargs()):
for prop_name, prop_value in kwargs_dict.items():
if prop_name not in exclude_props:
setattr(target_props, prop_name, prop_value)
RailingType = Literal["FRAMELESS_PANEL", "WALL_MOUNTED_HANDRAIL"]
CapType = Literal["TO_END_POST_AND_FLOOR", "TO_END_POST", "TO_FLOOR", "TO_WALL", "180", "NONE"]
@@ -1237,11 +1059,6 @@ class BIMRailingProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMRailingProperties") -> None:
"""Copy preset values to target railing properties."""
for prop_name, prop_value in self.get_general_kwargs().items():
setattr(target_props, prop_name, prop_value)
def to_angle(percentage: float) -> float:
return math.atan(percentage / 100)
@@ -1344,11 +1161,6 @@ class BIMRoofProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def copy_to(self, target_props: "BIMRoofProperties") -> None:
"""Copy preset values to target roof properties."""
for prop_name, prop_value in self.get_general_kwargs().items():
setattr(target_props, prop_name, prop_value)
class SnapMousePoint(PropertyGroup):
x: bpy.props.FloatProperty(name="X")
@@ -106,13 +106,6 @@ def update_railing_modifier_ifc_data(context: bpy.types.Context) -> None:
model_representation = ifcopenshell.api.geometry.add_railing_representation(ifc_file, **representation_data)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# recalculate normals to ensure correct shading
mesh = obj.data
if isinstance(mesh, bpy.types.Mesh):
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=False)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
tool.Blender.apply_bmesh(mesh, bm)
elif props.railing_type == "FRAMELESS_PANEL":
tool.Model.add_body_representation(obj)
@@ -342,8 +335,6 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_railing_props(obj)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
tool.Blender.get_addon_preferences().default_parameters.railing.copy_to(props)
railing_data = props.get_general_kwargs(convert_to_project_units=True)
path_data = get_path_data(obj)
@@ -570,8 +570,6 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_roof_props(obj)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
tool.Blender.get_addon_preferences().default_parameters.roof.copy_to(props)
# rejecting original roof shape to be safe
# taking into account only it's bounding box dimensions
if obj.dimensions.x == 0 or obj.dimensions.y == 0:
+2 -393
View File
@@ -19,7 +19,6 @@
import bpy
import json
import bmesh
import math
import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.util.element
@@ -27,14 +26,11 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.core.root
import bonsai.tool as tool
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from mathutils import Vector, Matrix
from mathutils import Vector
from bmesh.types import BMVert
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from typing import get_args
def regenerate_stair_mesh(obj: bpy.types.Object) -> None:
@@ -135,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
@@ -192,8 +188,6 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_stair_props(obj)
ifc_file = tool.Ifc.get()
tool.Blender.get_addon_preferences().default_parameters.stair.copy_to(props)
# Use the special method that includes custom_tread_lock for IFC storage
stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
@@ -297,388 +291,3 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
class ToggleStairTotalLengthLock(bpy.types.Operator):
"""Toggle the total length lock for stair editing"""
bl_idname = "bim.toggle_stair_total_length_lock"
bl_label = "Toggle Stair Total Length Lock"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
props.total_length_lock = not props.total_length_lock
return {"FINISHED"}
class AdjustStairTreads(bpy.types.Operator):
"""Adjust the number of treads"""
bl_idname = "bim.adjust_stair_treads"
bl_label = "Adjust Stair Treads"
bl_options = {"REGISTER", "UNDO"}
increment: IntProperty(name="Increment", default=1)
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
new_value = props.number_of_treads + self.increment
if new_value >= 1:
props.number_of_treads = new_value
return {"FINISHED"}
class CycleStairType(bpy.types.Operator):
"""Cycle through stair types. Shift+click to cycle in reverse."""
bl_idname = "bim.cycle_stair_type"
bl_label = "Cycle Stair Type"
bl_options = {"REGISTER", "UNDO"}
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
def invoke(self, context, event):
self.reverse = event.shift
return self.execute(context)
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
stair_types = get_args(tool.Model.StairType)
current_idx = stair_types.index(props.stair_type) if props.stair_type in stair_types else 0
direction = -1 if self.reverse else 1
props.stair_type = stair_types[(current_idx + direction) % len(stair_types)]
return {"FINISHED"}
class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_stair_edition"
bl_label = "Stair Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
# Gizmo layout offsets (meters)
GIZMO_X_OFFSET = 0.5 # Horizontal offset from stair end
GIZMO_PLUS_X_OFFSET = 0.25 # Additional X offset for plus button
GIZMO_MINUS_X_OFFSET = 0.5 # Additional X offset for minus button
GIZMO_BUTTON_Z_OFFSET = 0.15 # Vertical offset for +/- buttons above lock
GIZMO_CYCLE_Z_OFFSET = 0.5 # Vertical offset for cycle gizmo above lock
enable_editing_operator = "bim.enable_editing_stair"
finish_editing_operator = "bim.finish_editing_stair"
cancel_editing_operator = "bim.cancel_editing_stair"
gizmo_props = [
GizmoPropConfig("width", (0, 1, 0)),
GizmoPropConfig("height", (0, 0, 1)),
GizmoPropConfig("tread_run", (1, 0, 0)),
GizmoPropConfig("tread_depth", (0, 0, -1)),
GizmoPropConfig("nosing_length", (-1, 0, 0)),
GizmoPropConfig("nosing_depth", (0, 0, -1)),
GizmoPropConfig("total_length_target", (1, 0, 0)),
GizmoPropConfig("base_slab_depth", (0, 0, -1)),
GizmoPropConfig("top_slab_depth", (0, 0, -1)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_stair(element)
def get_props(self, obj):
return tool.Model.get_stair_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.stair
def should_hide_gizmo(self, attr_name, props):
"""Stair-specific visibility rules for gizmos."""
if not props.is_editing:
return True
# Hide concrete-specific gizmos for non-concrete stairs
if attr_name in ("base_slab_depth", "top_slab_depth") and props.stair_type != "CONCRETE":
return True
# Hide tread_depth for generic stairs (has no tread geometry)
if attr_name == "tread_depth" and props.stair_type == "GENERIC":
return True
# Hide nosing_depth when nosing_length is 0 or for Wood/Steel stair types
if attr_name == "nosing_depth" and (props.nosing_length == 0.0 or props.stair_type == "WOOD/STEEL"):
return True
return False
@staticmethod
def _get_stair_total_run(props) -> float:
"""Calculate the total horizontal run of the stair."""
return props.tread_run * props.number_of_treads
@staticmethod
def _get_first_riser_height(props) -> float:
"""Calculate the height of the first riser."""
return props.height / (props.number_of_treads + 1)
def get_gizmo_matrix_width(self, props):
translation = Matrix.Translation(Vector((0, props.width, 0)))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_width_top(self, props):
"""Position for the secondary width gizmo at the top of the stairs."""
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width, props.height)))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_height(self, props):
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height)))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_tread_run(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((props.tread_run, props.width / 2, riser_height)))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_tread_depth(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(
Vector((props.tread_run / 2, props.width / 2, riser_height - props.tread_depth))
)
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_nosing_length(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((-props.nosing_length, props.width / 2, riser_height)))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_nosing_depth(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(
Vector((-props.nosing_length, props.width / 2, riser_height - props.nosing_depth))
)
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_total_length_target(self, props):
translation = Matrix.Translation(Vector((props.total_length_target, props.width / 2, props.height)))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_base_slab_depth(self, props):
translation = Matrix.Translation(Vector((0, props.width / 2, -props.base_slab_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_top_slab_depth(self, props):
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height - props.top_slab_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def setup(self, context):
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
# Stair-specific gizmos
prefs = tool.Blender.get_addon_preferences()
highlight_color = prefs.decorator_color_selected[:3]
# Secondary width gizmo at the top of the stairs (linked to same width property)
self.gizmo_width_top = self.gizmos.new("BIM_GT_gizmo_arrow_2d")
def make_width_get():
def move_get():
obj = bpy.context.active_object
if not obj:
return 0.0
props = self.get_props(obj)
return props.width
return move_get
def make_width_set():
def move_set(value):
obj = bpy.context.active_object
if not obj:
return
props = self.get_props(obj)
props.width = max(0.0, value)
return move_set
self.gizmo_width_top.move_get_cb = make_width_get()
self.gizmo_width_top.move_set_cb = make_width_set()
self.gizmo_width_top.axis = Vector((0, 1, 0))
self.gizmo_width_top.local_axis = Vector((0, 1, 0))
self.gizmo_width_top.invert_delta = False
self.gizmo_width_top.delta_scale = 1.0
self.gizmo_width_top.prop_name = "Width"
self.gizmo_width_top.gizmo_group = self
self.gizmo_width_top.color = self.COLOR_GREEN
self.gizmo_width_top.color_highlight = highlight_color
self.gizmo_width_top.alpha = 0.99
self.gizmo_width_top.use_draw_modal = True
self.gizmo_width_top.use_draw_scale = True
# Total length lock gizmo
self.lock_gizmo = self.gizmos.new("VIEW3D_GT_lock")
self.lock_gizmo.use_draw_scale = False
self.lock_gizmo.color = self.COLOR_BLUE
self.lock_gizmo.color_highlight = highlight_color
self.lock_gizmo.alpha = 0.8
self.lock_gizmo.prop_path = "BIMStairProperties.total_length_lock"
self.lock_gizmo.target_set_operator("bim.toggle_stair_total_length_lock")
# Plus gizmo for increasing treads
self.plus_gizmo = self.gizmos.new("VIEW3D_GT_plus")
self.plus_gizmo.use_draw_scale = False
self.plus_gizmo.color = self.COLOR_GREEN
self.plus_gizmo.color_highlight = highlight_color
self.plus_gizmo.alpha = 0.8
op = self.plus_gizmo.target_set_operator("bim.adjust_stair_treads")
op.increment = 1
# Minus gizmo for decreasing treads
self.minus_gizmo = self.gizmos.new("VIEW3D_GT_minus")
self.minus_gizmo.use_draw_scale = False
self.minus_gizmo.color = self.COLOR_RED
self.minus_gizmo.color_highlight = highlight_color
self.minus_gizmo.alpha = 0.8
op = self.minus_gizmo.target_set_operator("bim.adjust_stair_treads")
op.increment = -1
# Cycle gizmo for stair type
default_color = prefs.decorations_colour[:3]
self.cycle_gizmo = self.gizmos.new("VIEW3D_GT_cycle")
self.cycle_gizmo.use_draw_scale = False
self.cycle_gizmo.color = default_color
self.cycle_gizmo.color_highlight = highlight_color
self.cycle_gizmo.alpha = 0.8
self.cycle_gizmo.target_set_operator("bim.cycle_stair_type")
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
billboard_rot = gizmo.get_billboard_rotation(context)
self.update_property_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
self.update_width_top_gizmo(mw, props)
self.update_lock_gizmo(mw, props, billboard_rot)
self.update_tread_count_gizmos(mw, props, billboard_rot)
self.update_cycle_gizmo(mw, props, billboard_rot)
def update_width_top_gizmo(self, mw, props):
"""Update the secondary width gizmo at the top of the stairs."""
gizmo_prefs = self.get_gizmo_prefs()
# Use same visibility as the main width gizmo
self.gizmo_width_top.hide = not props.is_editing or not getattr(gizmo_prefs, "width", True)
if self.gizmo_width_top.hide:
return
self.gizmo_width_top.matrix_basis = mw @ self.get_gizmo_matrix_width_top(props)
self.gizmo_width_top.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
def update_lock_gizmo(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.lock_gizmo.hide = not props.is_editing or not gizmo_prefs.lock
if self.lock_gizmo.hide:
return
self.lock_gizmo.color = self.COLOR_RED if props.total_length_lock else self.COLOR_GREEN
total_run = self._get_stair_total_run(props)
lock_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
local_transform = (
Matrix.Translation(Vector((lock_x, props.width / 2, props.height)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.lock_gizmo.matrix_basis = mw @ local_transform
def update_tread_count_gizmos(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.plus_gizmo.hide = not props.is_editing or not gizmo_prefs.plus
self.minus_gizmo.hide = not props.is_editing or props.number_of_treads <= 1 or not gizmo_prefs.minus
if self.plus_gizmo.hide and self.minus_gizmo.hide:
return
total_run = self._get_stair_total_run(props)
base_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
if not self.plus_gizmo.hide:
plus_local_transform = (
Matrix.Translation(Vector((
base_x + self.GIZMO_PLUS_X_OFFSET,
props.width / 2,
props.height + self.GIZMO_BUTTON_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.plus_gizmo.matrix_basis = mw @ plus_local_transform
if not self.minus_gizmo.hide:
minus_local_transform = (
Matrix.Translation(Vector((
base_x + self.GIZMO_MINUS_X_OFFSET,
props.width / 2,
props.height + self.GIZMO_BUTTON_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.minus_gizmo.matrix_basis = mw @ minus_local_transform
def update_cycle_gizmo(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.cycle_gizmo.hide = not props.is_editing or not gizmo_prefs.cycle
if self.cycle_gizmo.hide:
return
total_run = self._get_stair_total_run(props)
cycle_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
local_transform = (
Matrix.Translation(Vector((
cycle_x,
props.width / 2,
props.height + self.GIZMO_CYCLE_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.cycle_gizmo.matrix_basis = mw @ local_transform
def draw_prepare(self, context):
"""Called before drawing - updates gizmos to face camera."""
# Call base class implementation
super().draw_prepare(context)
# Also update the secondary width gizmo to face camera
if hasattr(self, "gizmo_width_top") and not self.gizmo_width_top.hide:
self.gizmo_width_top.draw_prepare(context)
+96 -132
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")
@@ -305,9 +301,48 @@ class BIM_PT_stair(bpy.types.Panel):
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
if prop_name == "custom_tread_lock":
continue
draw_stair_properties(self.layout, props)
prop_value = getattr(props, prop_name)
# Special handling for custom_first_last_tread_run
if prop_name == "custom_first_last_tread_run":
# Draw the lock toggle
row_lock = self.layout.row(align=True)
lock_text = (
"Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
)
row_lock.prop(
props,
"custom_tread_lock",
text=lock_text,
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
)
# Only show the custom values input if unlocked
if not props.custom_tread_lock:
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
else:
self.layout.prop(props, prop_name)
if prop_name == "height": # Weak but we just want to insert this inside props drawing
row_length = self.layout.row(align=True)
row_length.prop(props, "total_length_target")
row_length.prop(
props,
"total_length_lock",
text="",
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
)
regenerate_stair_mesh(obj)
else:
calculated_params = StairData.data["calculated_params"]
@@ -401,11 +436,39 @@ class BIM_PT_window(bpy.types.Panel):
row.label(text="Window parameters", icon="OUTLINER_OB_LATTICE")
if props.is_editing:
number_of_panels, panels_data = props.window_types_panels[props.window_type]
row = self.layout.row(align=True)
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
draw_window_properties(self.layout, props)
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
lining_props = props.get_lining_kwargs()
self.layout.label(text="Lining properties")
for prop in lining_props:
self.layout.prop(props, prop)
panel_props = props.get_panel_kwargs()
self.layout.label(text="Panel properties")
panel_box = self.layout.box()
row = panel_box.row()
cols = [row.column(align=True) for i in range(number_of_panels + 1)]
cols[0].label(text="")
for panel_i in range(number_of_panels):
r = cols[panel_i + 1].row()
r.alignment = "CENTER"
r.label(text=f"#{panel_i}")
r = cols[panel_i + 1].row()
for prop in panel_props:
cols[0].label(text=f"{props.bl_rna.properties[prop].name}")
for panel_i in range(number_of_panels):
cols[panel_i + 1].prop(props, prop, index=panel_i, text="")
self.layout.use_property_split = True
self.layout.label(text="Material Properties")
@@ -493,7 +556,19 @@ class BIM_PT_door(bpy.types.Panel):
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
draw_door_properties(self.layout, props)
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
lining_props = props.get_lining_kwargs()
self.layout.label(text="Lining Properties")
for prop in lining_props:
self.layout.prop(props, prop)
panel_props = props.get_panel_kwargs()
self.layout.label(text="Panel Properties")
for prop in panel_props:
self.layout.prop(props, prop)
self.layout.use_property_split = True
self.layout.label(text="Material Properties")
@@ -564,7 +639,14 @@ class BIM_PT_railing(bpy.types.Panel):
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
draw_railing_properties(self.layout, props)
general_props = props.get_general_kwargs()
for prop in general_props:
if prop == "support_spacing" and props.use_manual_supports:
row = self.layout.row()
row.prop(props, prop)
row.active = False
continue
self.layout.prop(props, prop)
update_railing_modifier_bmesh(context)
@@ -622,7 +704,9 @@ class BIM_PT_roof(bpy.types.Panel):
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
draw_roof_properties(self.layout, props)
general_props = props.get_general_kwargs()
for prop in general_props:
self.layout.prop(props, prop)
update_roof_modifier_bmesh(obj)
elif props.is_editing_path:
@@ -688,126 +772,6 @@ class BIM_PT_external_parametric_geometry(bpy.types.Panel):
row.prop(input, "default_value", text=input.name)
def draw_door_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw door properties UI (shared between properties panel and preferences)."""
# General properties
general_props = props.get_general_kwargs()
for prop in general_props:
layout.prop(props, prop)
# Lining properties
layout.label(text="Lining Properties")
lining_props = props.get_lining_kwargs()
for prop in lining_props:
layout.prop(props, prop)
# Panel properties
layout.label(text="Panel Properties")
panel_props = props.get_panel_kwargs()
for prop in panel_props:
layout.prop(props, prop)
def draw_window_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw window properties UI (shared between properties panel and preferences)."""
number_of_panels, panels_data = props.window_types_panels[props.window_type]
# General and lining properties
general_props = props.get_general_kwargs()
for prop in general_props:
layout.prop(props, prop)
layout.label(text="Lining Properties")
lining_props = props.get_lining_kwargs()
for prop in lining_props:
layout.prop(props, prop)
# Panel properties (special layout for multiple panels)
panel_props = props.get_panel_kwargs()
layout.label(text="Panel Properties")
panel_box = layout.box()
row = panel_box.row()
cols = [row.column(align=True) for i in range(number_of_panels + 1)]
cols[0].label(text="")
for panel_i in range(number_of_panels):
r = cols[panel_i + 1].row()
r.alignment = "CENTER"
r.label(text=f"#{panel_i}")
r = cols[panel_i + 1].row()
for prop in panel_props:
cols[0].label(text=f"{props.bl_rna.properties[prop].name}")
for panel_i in range(number_of_panels):
cols[panel_i + 1].prop(props, prop, index=panel_i, text="")
def draw_railing_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw railing properties UI (shared between properties panel and preferences)."""
general_props = props.get_general_kwargs()
for prop in general_props:
if prop == "support_spacing" and props.use_manual_supports:
row = layout.row()
row.prop(props, prop)
row.active = False
continue
layout.prop(props, prop)
def draw_roof_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw roof properties UI (shared between properties panel and preferences)."""
# General properties
general_props = props.get_general_kwargs()
for prop in general_props:
layout.prop(props, prop)
def draw_stair_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None:
"""Draw stair properties UI (shared between properties panel and preferences)."""
for prop_name in props.get_props_kwargs():
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
if prop_name == "custom_tread_lock":
continue
prop_value = getattr(props, prop_name)
# Special handling for custom_first_last_tread_run
if prop_name == "custom_first_last_tread_run":
# Draw the lock toggle
row_lock = layout.row(align=True)
lock_text = "Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
row_lock.prop(
props,
"custom_tread_lock",
text=lock_text,
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
)
# Only show the custom values input if unlocked
if not props.custom_tread_lock:
prop_readable_name = props.bl_rna.properties[prop_name].name
layout.label(text=f"{prop_readable_name}:")
layout.prop(props, prop_name, text="")
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
prop_readable_name = props.bl_rna.properties[prop_name].name
layout.label(text=f"{prop_readable_name}:")
layout.prop(props, prop_name, text="")
else:
layout.prop(props, prop_name)
if prop_name == "height": # Weak but we just want to insert this inside props drawing
row_length = layout.row(align=True)
row_length.prop(props, "total_length_target")
row_length.prop(
props,
"total_length_lock",
text="",
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
)
def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
self.layout.operator_context = "INVOKE_REGION_WIN"
self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element")
+5 -239
View File
@@ -28,8 +28,6 @@ import ifcopenshell.api.pset
import bonsai.tool as tool
import bonsai.core.root
import bonsai.core.geometry
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS
import ifcopenshell.api
import ifcopenshell.api.material
@@ -38,8 +36,8 @@ import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
from bmesh.types import BMVert
from mathutils import Vector, Matrix
from typing import get_args
from mathutils import Vector
from typing import Optional, Union
V_ = tool.Blender.V_
@@ -141,7 +139,7 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None:
def create_bm_window_frame(
bm: bmesh.types.BMesh, size: Vector, thickness: float | list[float], position: Vector = V_(0, 0, 0).freeze()
bm: bmesh.types.BMesh, size: Vector, thickness: Union[float, list[float]], position: Vector = V_(0, 0, 0).freeze()
) -> list[bmesh.types.BMVert]:
"""`thickness` of the profile is defined as list in the following order:
`(LEFT, TOP, RIGHT, BOTTOM)`
@@ -228,7 +226,7 @@ def create_bm_window(
frame_thickness: float,
glass_thickness: float,
position: Vector,
x_offsets: list | None = None,
x_offsets: Optional[list] = None,
) -> tuple[list[bmesh.types.BMVert], list[bmesh.types.BMVert], list[bmesh.types.BMVert]]:
"""`lining_thickness` and `x_offsets` are expected to be defined as a list,
similarly to `create_bm_window_frame` `thickness` argument"""
@@ -260,9 +258,6 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
obj = context.active_object
assert obj
props = tool.Model.get_window_props(obj)
if not props.is_editing:
return
panel_schema = DEFAULT_PANEL_SCHEMAS[props.window_type]
accumulated_height = [0] * len(panel_schema[0])
built_panels = []
@@ -393,7 +388,6 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=bm.verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if bpy.context.active_object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data)
@@ -405,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
@@ -457,8 +451,6 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
assert element
props = tool.Model.get_window_props(obj)
tool.Blender.get_addon_preferences().default_parameters.window.copy_to(props)
window_data = props.get_general_kwargs(convert_to_project_units=True)
lining_props = props.get_lining_kwargs(convert_to_project_units=True)
panel_props = props.get_panel_kwargs(convert_to_project_units=True)
@@ -578,229 +570,3 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
class CycleWindowType(bpy.types.Operator, tool.Ifc.Operator):
"""Cycle through available window types."""
bl_idname = "bim.cycle_window_type"
bl_label = "Cycle Window Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element or not tool.Blender.Modifier.is_window(element):
return {"CANCELLED"}
props = tool.Model.get_window_props(obj)
window_types = list(get_args(tool.Model.WindowType))
current_index = window_types.index(props.window_type) if props.window_type in window_types else 0
next_index = (current_index + 1) % len(window_types)
props.window_type = window_types[next_index]
return {"FINISHED"}
class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_window_edition"
bl_label = "Window Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
enable_editing_operator = "bim.enable_editing_window"
finish_editing_operator = "bim.finish_editing_window"
cancel_editing_operator = "bim.cancel_editing_window"
cycle_type_operator = "bim.cycle_window_type"
gizmo_props = [
GizmoPropConfig("overall_height", (0, 0, 1)),
GizmoPropConfig("overall_width", (1, 0, 0)),
GizmoPropConfig("lining_depth", (0, 1, 0)),
GizmoPropConfig("lining_thickness", (1, 0, 0)),
GizmoPropConfig("lining_to_panel_offset_x", (1, 0, 0)),
GizmoPropConfig("lining_to_panel_offset_y", (0, 1, 0)),
GizmoPropConfig("mullion_thickness", (1, 0, 0), delta_scale=2.0),
GizmoPropConfig("first_mullion_offset", (1, 0, 0)),
GizmoPropConfig("second_mullion_offset", (1, 0, 0)),
GizmoPropConfig("transom_thickness", (0, 0, 1), delta_scale=2.0),
GizmoPropConfig("first_transom_offset", (0, 0, 1)),
GizmoPropConfig("second_transom_offset", (0, 0, 1)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_window(element)
def get_props(self, obj):
return tool.Model.get_window_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.window
def should_hide_gizmo(self, attr_name, props):
"""Window-specific visibility rules for gizmos."""
if not props.is_editing:
return True
has_mullion = self._has_mullion(props)
has_second_mullion = self._has_second_mullion(props)
has_transom = self._has_transom(props)
has_second_transom = self._has_second_transom(props)
if attr_name == "mullion_thickness" and not has_mullion:
return True
if attr_name == "first_mullion_offset" and not has_mullion:
return True
if attr_name == "second_mullion_offset" and not has_second_mullion:
return True
if attr_name == "transom_thickness" and not has_transom:
return True
if attr_name == "first_transom_offset" and not has_transom:
return True
if attr_name == "second_transom_offset" and not has_second_transom:
return True
return False
def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_overall_width(self, props):
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_lining_depth(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_lining_thickness(self, props):
translation = Matrix.Translation(
V_(props.lining_thickness, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
@staticmethod
def _get_lining_to_panel_offset_y_full(props) -> float:
"""Get the full Y offset for lining-to-panel positioning."""
return (props.lining_depth - props.frame_depth[0]) + props.lining_to_panel_offset_y
def get_gizmo_matrix_lining_to_panel_offset_x(self, props):
y_full = self._get_lining_to_panel_offset_y_full(props)
translation = Matrix.Translation(
V_(props.lining_to_panel_offset_x, y_full + props.lining_offset, props.lining_thickness)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_lining_to_panel_offset_y(self, props):
y_full = self._get_lining_to_panel_offset_y_full(props)
translation = Matrix.Translation(
V_(
props.lining_to_panel_offset_x,
y_full + props.frame_depth[0] + props.lining_offset,
props.lining_thickness,
)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_mullion_thickness(self, props):
# Position at the right edge of the mullion (offset + thickness/2) so gizmo movement matches property 1:1
translation = Matrix.Translation(
V_(props.first_mullion_offset + props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_first_mullion_offset(self, props):
# Position at the left edge of the mullion (offset - thickness/2)
translation = Matrix.Translation(
V_(props.first_mullion_offset - props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_second_mullion_offset(self, props):
translation = Matrix.Translation(V_(props.second_mullion_offset, props.lining_offset, props.overall_height / 2))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props):
# Position at the top edge of the transom (offset + thickness/2) so gizmo movement matches property 1:1
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset + props.transom_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_first_transom_offset(self, props):
# Position at the bottom edge of the transom (offset - thickness/2)
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset - props.transom_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_second_transom_offset(self, props):
translation = Matrix.Translation(V_(props.overall_width / 2, props.lining_offset, props.second_transom_offset))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def _has_mullion(self, props):
"""Check if the window type uses mullions (vertical dividers)."""
window_type = props.window_type
return window_type in (
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_VERTICAL",
)
def _has_second_mullion(self, props):
"""Check if the window type uses a second mullion."""
return props.window_type == "TRIPLE_PANEL_VERTICAL"
def _has_transom(self, props):
"""Check if the window type uses transoms (horizontal dividers)."""
window_type = props.window_type
return window_type in (
"DOUBLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_HORIZONTAL",
)
def _has_second_transom(self, props):
"""Check if the window type uses a second transom."""
return props.window_type == "TRIPLE_PANEL_HORIZONTAL"
def setup(self, context):
# Use base class methods for common gizmos
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
self.update_property_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
@@ -1322,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:
@@ -635,7 +635,6 @@ class BIM_PT_purge(Panel):
bl_parent_id = "BIM_PT_tab_quality_control"
def draw(self, context):
props = tool.Debug.get_debug_props()
layout = self.layout
layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE"
layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE"
@@ -656,23 +655,3 @@ class BIM_PT_purge(Panel):
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
merge_op.object_type = object_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and (data := context.active_object.data):
mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
row = layout.row(align=True)
row.prop(ifc_parameter, "name", text="")
row.prop(ifc_parameter, "value", text="")
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
+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()
+9 -38
View File
@@ -36,7 +36,6 @@ import bonsai.core.geometry
import ifcopenshell
import ifcopenshell.api.attribute
import ifcopenshell.util.unit
from bonsai.tool.unit import parse_distance_string
from typing import TYPE_CHECKING, Union, Literal
@@ -49,46 +48,18 @@ def get_subelement_class(
def update_elevation(self: "BIMContainer", context: bpy.types.Context) -> None:
# Try to parse the input string with unit support first
is_valid, parsed_elevation = parse_distance_string(self.elevation, use_project_unit=True)
if is_valid:
elevation = parsed_elevation
# Format the string if it needs normalization:
# - Plain numbers (no units): "4" → "4' - 0""
# - Missing symbols: "5'3" → "5' - 3""
# - Inconsistent formatting: "5' 3 3/256" → "5' - 3 3/256""
input_str = self.elevation.strip()
# Check if input needs formatting (has feet/inch symbols or is plain number)
needs_formatting = (
input_str.replace(".", "").replace("-", "").replace(" ", "").replace("/", "").isdigit() # Plain number
or "'" in input_str # Has feet symbol
or '"' in input_str # Has inch symbol
or " " in input_str.replace(" - ", "") # Has spaces (but not the formatted " - ")
)
# Only format if the current string doesn't match our standard format
if needs_formatting:
formatted = tool.Unit.format_distance(elevation)
if self.elevation != formatted:
self.elevation = formatted
return
else:
# Fall back to direct float conversion for backward compatibility
try:
elevation = float(self.elevation)
except Exception as e:
print(f"Elevation parsing failed for '{self.elevation}': {e}")
elevation = 0
# Update the object's position in the 3D scene
try:
elevation = float(self.elevation)
if self.elevation != str(elevation):
self.elevation = str(elevation)
return
except:
elevation = 0
if ifc_definition_id := self.ifc_definition_id:
entity = tool.Ifc.get().by_id(ifc_definition_id)
if obj := tool.Ifc.get_object(entity):
# elevation is already in meters, set it directly
obj.matrix_world[2][3] = elevation
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
obj.matrix_world[2][3] = elevation * unit_scale
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj)
+1 -1
View File
@@ -277,7 +277,7 @@ class BIM_UL_containers_manager(UIList):
if item:
row = layout.row(align=True)
icon = self.icon_by_class.get(item.ifc_class, "META_PLANE")
split = row.split(factor=0.8)
split = row.split(factor=0.85)
if item.long_name:
split2 = split.split(factor=0.7)
row = split2.row(align=True)
@@ -455,7 +455,7 @@ class ActivateExternalStyle(bpy.types.Operator):
return {"CANCELLED"}
self.copy_material_attributes(db["data_block"], material)
if 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:
+1 -41
View File
@@ -322,15 +322,6 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Duplicate Type"
bl_options = {"REGISTER", "UNDO"}
element: bpy.props.IntProperty()
name: bpy.props.StringProperty(name="Name")
description: bpy.props.StringProperty(name="Description")
assign_selected_objects: bpy.props.BoolProperty(default=False)
if TYPE_CHECKING:
element: int
name: str
description: str
assign_selected_objects: bool
def _execute(self, context):
element = tool.Ifc.get().by_id(self.element)
@@ -341,25 +332,8 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
if obj.data:
new_obj.data = obj.data.copy()
new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
# Apply the name and description from the dialog
new.Name = self.name
if self.description:
new.Description = self.description
new.Name += " Copy"
bpy.ops.bim.load_type_thumbnails()
# Assign selected objects to the new type if requested
if self.assign_selected_objects:
selected_objects = tool.Blender.get_selected_objects()
prefs = tool.Blender.get_addon_preferences()
for selected_obj in selected_objects:
selected_element = tool.Ifc.get_entity(selected_obj)
if selected_element and selected_element.is_a("IfcObject"):
core.assign_type(tool.Ifc, tool.Type, element=selected_element, type=new)
if prefs.occurrence_name_style == "TYPE":
selected_obj.name = tool.Model.generate_occurrence_name(new, selected_element.is_a())
if obj in context.selectable_objects:
tool.Blender.select_and_activate_single_object(context, new_obj)
else:
@@ -373,17 +347,3 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
props.ifc_class = new.is_a()
props.relating_type_id = str(tool.Blender.get_ifc_definition_id(new_obj))
return {"FINISHED"}
def invoke(self, context, event):
element = tool.Ifc.get().by_id(self.element)
self.name = (element.Name or "Unnamed") + " Copy"
self.description = element.Description or ""
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
self.layout.prop(self, "name")
self.layout.prop(self, "description")
selected_objects = tool.Blender.get_selected_objects()
ifc_objects = [obj for obj in selected_objects if tool.Ifc.get_entity(obj) and tool.Ifc.get_entity(obj).is_a("IfcObject")]
if ifc_objects:
self.layout.prop(self, "assign_selected_objects", text=f"Assign {len(ifc_objects)} Selected Object(s) to New Type")
-4
View File
@@ -96,10 +96,6 @@ class BIM_PT_type(Panel):
op = row.operator("bim.select_type", icon="OBJECT_DATA", text="")
op.relating_type = 0 # will only select the relating types of only the selected objects
row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="")
# Add duplicate button here with assign_selected_objects enabled
op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="")
op.element = TypeData.data["relating_type"]["id"]
op.assign_selected_objects = True
row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="")
row.operator("bim.unassign_type", icon="X", text="")
else:
@@ -102,19 +102,6 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
element_had_openings = tool.Geometry.has_openings(voided_element)
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body")
if not element2:
# Apply scale transformation for vanilla Blender objects before creating IFC representation
# This ensures the opening geometry matches the visual scale of the object
if tool.Geometry.is_scaled(obj2):
if obj2.data:
# Make mesh data single-user if it's shared
if obj2.data.users > 1:
obj2.data = obj2.data.copy()
context_override = {}
context_override["object"] = context_override["active_object"] = obj2
context_override["selected_objects"] = context_override["selected_editable_objects"] = [obj2]
with bpy.context.temp_override(**context_override):
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
element2 = bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
+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 -276
View File
@@ -38,20 +38,6 @@ from ifcopenshell.util.file import IfcHeaderExtractor
from bonsai.bim.prop import Attribute
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
from bonsai.bim.module.pset.prop import IfcProperty
from bonsai.bim.module.model.prop import (
BIMDoorProperties,
BIMWindowProperties,
BIMRailingProperties,
BIMRoofProperties,
BIMStairProperties,
)
from bonsai.bim.module.model.ui import (
draw_door_properties,
draw_window_properties,
draw_railing_properties,
draw_roof_properties,
draw_stair_properties,
)
from typing import Optional, TYPE_CHECKING, Literal
from natsort import natsorted
import textwrap
@@ -242,120 +228,6 @@ class BIM_UL_generic(bpy.types.UIList):
layout.label(text="", translate=False)
class GizmoPreferencesDoor(bpy.types.PropertyGroup):
"""Property group for door gizmo visibility settings."""
overall_height: BoolProperty(name="Overall Height", default=True)
overall_width: BoolProperty(name="Overall Width", default=True)
threshold_thickness: BoolProperty(name="Threshold Thickness", default=True)
threshold_depth: BoolProperty(name="Threshold Depth", default=True)
lining_depth: BoolProperty(name="Lining Depth", default=True)
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
transom_offset: BoolProperty(name="Transom Offset", default=True)
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
casing_thickness: BoolProperty(name="Casing Thickness", default=True)
casing_depth: BoolProperty(name="Casing Depth", default=True)
swing_arc: BoolProperty(name="Swing Arc", default=True, description="Show door swing direction arc")
flip_arc: BoolProperty(name="Flip Arc", default=True, description="Show flip door orientation arc")
if TYPE_CHECKING:
overall_height: bool
overall_width: bool
threshold_thickness: bool
threshold_depth: bool
lining_depth: bool
lining_thickness: bool
transom_offset: bool
transom_thickness: bool
casing_thickness: bool
casing_depth: bool
swing_arc: bool
flip_arc: bool
class GizmoPreferencesWindow(bpy.types.PropertyGroup):
"""Property group for window gizmo visibility settings."""
overall_height: BoolProperty(name="Overall Height", default=True)
overall_width: BoolProperty(name="Overall Width", default=True)
lining_depth: BoolProperty(name="Lining Depth", default=True)
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
lining_to_panel_offset_x: BoolProperty(name="Lining to Panel Offset X", default=True)
lining_to_panel_offset_y: BoolProperty(name="Lining to Panel Offset Y", default=True)
mullion_thickness: BoolProperty(name="Mullion Thickness", default=True)
first_mullion_offset: BoolProperty(name="First Mullion Offset", default=True)
second_mullion_offset: BoolProperty(name="Second Mullion Offset", default=True)
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
first_transom_offset: BoolProperty(name="First Transom Offset", default=True)
second_transom_offset: BoolProperty(name="Second Transom Offset", default=True)
if TYPE_CHECKING:
overall_height: bool
overall_width: bool
lining_depth: bool
lining_thickness: bool
lining_to_panel_offset_x: bool
lining_to_panel_offset_y: bool
mullion_thickness: bool
first_mullion_offset: bool
second_mullion_offset: bool
transom_thickness: bool
first_transom_offset: bool
second_transom_offset: bool
class GizmoPreferencesStair(bpy.types.PropertyGroup):
"""Property group for stair gizmo visibility settings."""
width: BoolProperty(name="Width", default=True)
height: BoolProperty(name="Height", default=True)
tread_run: BoolProperty(name="Tread Run", default=True)
tread_depth: BoolProperty(name="Tread Depth", default=True)
nosing_length: BoolProperty(name="Nosing Length", default=True)
nosing_depth: BoolProperty(name="Nosing Depth", default=True)
total_length_target: BoolProperty(name="Total Length Target", default=True)
base_slab_depth: BoolProperty(name="Base Slab Depth", default=True)
top_slab_depth: BoolProperty(name="Top Slab Depth", default=True)
lock: BoolProperty(name="Total Length Lock", default=True)
plus: BoolProperty(name="Add Tread (+)", default=True)
minus: BoolProperty(name="Remove Tread (-)", default=True)
cycle: BoolProperty(name="Cycle Stair Type", default=True)
if TYPE_CHECKING:
width: bool
height: bool
tread_run: bool
tread_depth: bool
nosing_length: bool
nosing_depth: bool
total_length_target: bool
base_slab_depth: bool
top_slab_depth: bool
lock: bool
plus: bool
minus: bool
cycle: bool
class GizmoPreferences(bpy.types.PropertyGroup):
"""Property group for all gizmo visibility settings."""
draw_gizmos_in_3d_viewport: BoolProperty(
name="Draw Gizmos In 3D Viewport",
default=True,
description="Show interactive gizmos in the 3D viewport for parametric elements",
)
door: bpy.props.PointerProperty(type=GizmoPreferencesDoor)
window: bpy.props.PointerProperty(type=GizmoPreferencesWindow)
stair: bpy.props.PointerProperty(type=GizmoPreferencesStair)
if TYPE_CHECKING:
draw_gizmos_in_3d_viewport: bool
door: GizmoPreferencesDoor
window: GizmoPreferencesWindow
stair: GizmoPreferencesStair
class DocPreferences(bpy.types.PropertyGroup):
sheets_dir: StringProperty(
default=os.path.join("sheets") + os.path.sep,
@@ -448,63 +320,29 @@ class DocPreferences(bpy.types.PropertyGroup):
classes_no_cut: str
class DefaultParameters(bpy.types.PropertyGroup):
door: bpy.props.PointerProperty(type=BIMDoorProperties)
window: bpy.props.PointerProperty(type=BIMWindowProperties)
railing: bpy.props.PointerProperty(type=BIMRailingProperties)
roof: bpy.props.PointerProperty(type=BIMRoofProperties)
stair: bpy.props.PointerProperty(type=BIMStairProperties)
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,
@@ -513,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",
@@ -534,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(
@@ -598,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(
@@ -629,7 +456,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
name="Occurrence Name Function",
description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style",
)
gizmos: bpy.props.PointerProperty(type=GizmoPreferences)
def update_data_dir(self, context: bpy.types.Context) -> None:
import bonsai.bim.schema
@@ -660,11 +486,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
subtype="DIR_PATH",
)
doc: bpy.props.PointerProperty(type=DocPreferences)
default_parameters: bpy.props.PointerProperty(
type=DefaultParameters,
name="Default Parameters",
description="Default parameters for BIM elements",
)
if TYPE_CHECKING:
svg2pdf_command: str
@@ -677,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]
@@ -695,12 +515,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_always_cache: bool
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
occurrence_name_function: str
gizmos: GizmoPreferences
data_dir: str
cache_dir: str
pset_dir: str
doc: DocPreferences
default_parameters: DefaultParameters
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
@@ -743,61 +561,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "should_use_snap")
layout.prop(self, "should_play_chaching_sound")
layout.prop(self.gizmos, "draw_gizmos_in_3d_viewport")
if self.gizmos.draw_gizmos_in_3d_viewport:
bonsai.bim.helper.draw_expandable_panel(
layout,
context,
"Gizmos Parameters",
self.draw_gizmo_parameters,
)
def draw_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.label(text="Toggle visibility of gizmos in editing mode")
box = layout.box()
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Door", self.draw_door_gizmo_parameters)
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Window", self.draw_window_gizmo_parameters)
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Stair", self.draw_stair_gizmo_parameters)
def draw_door_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.door import GizmoDoorEdition
door_gizmos = self.gizmos.door
gizmo_prop_names = {p.attr_name for p in GizmoDoorEdition.gizmo_props}
gizmo_prop_names.update(("swing_arc", "flip_arc"))
for prop in door_gizmos.__annotations__:
if prop in gizmo_prop_names:
layout.prop(door_gizmos, prop)
def draw_window_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.window import GizmoWindowEdition
window_gizmos = self.gizmos.window
gizmo_prop_names = {p.attr_name for p in GizmoWindowEdition.gizmo_props}
for prop in window_gizmos.__annotations__:
if prop in gizmo_prop_names:
layout.prop(window_gizmos, prop)
def draw_stair_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.stair import GizmoStairEdition
stair_gizmos = self.gizmos.stair
gizmo_prop_names = {p.attr_name for p in GizmoStairEdition.gizmo_props}
special_gizmo_names = {"lock", "plus", "minus"}
for prop in stair_gizmos.__annotations__:
if prop in gizmo_prop_names or prop in special_gizmo_names:
layout.prop(stair_gizmos, prop)
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "occurrence_name_style")
if self.occurrence_name_style == "CUSTOM":
layout.prop(self, "occurrence_name_function")
bonsai.bim.helper.draw_expandable_panel(
layout,
context,
"Default Parameters",
self.draw_default_parameters,
)
def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "data_dir")
@@ -834,39 +601,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.row().prop(self, "decorator_color_error")
layout.row().prop(self, "decorator_color_background")
def draw_default_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
box = layout.box()
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Door",
lambda _layout, _context: draw_door_properties(_layout, self.default_parameters.door),
)
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Window",
lambda _layout, _context: draw_window_properties(_layout, self.default_parameters.window),
)
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Railing",
lambda _layout, _context: draw_railing_properties(_layout, self.default_parameters.railing),
)
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Roof",
lambda _layout, _context: draw_roof_properties(_layout, self.default_parameters.roof),
)
bonsai.bim.helper.draw_expandable_panel(
box,
context,
"Stair",
lambda _layout, _context: draw_stair_properties(_layout, self.default_parameters.stair),
)
def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "opening_focus_opacity")
layout.prop(self, "should_disable_undo_on_save")
+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

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