mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8326feae3 |
@@ -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"
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-logs-osx-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -35,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'
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,7 +52,7 @@ Contents
|
||||
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
||||
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
||||
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
|
||||
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||
|
||||
+700
-26
@@ -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,13 +1048,18 @@ if(BUILD_IFCGEOM)
|
||||
endif()
|
||||
|
||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
if(WITH_OPENCASCADE)
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
set(SERIALIZER_SCHEMA_LIBRARIES ${SERIALIZER_SCHEMA_LIBRARIES} Serializers_ifc${schema})
|
||||
endforeach()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} Serializers ${SERIALIZER_SCHEMA_LIBRARIES})
|
||||
|
||||
if(WITH_OPENCASCADE)
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
set(GEOM_SERIALIZER_SCHEMA_LIBRARIES ${GEOM_SERIALIZER_SCHEMA_LIBRARIES} GeometrySerializers_ifc${schema})
|
||||
|
||||
add_library(geometry_serializer_ifc${schema} STATIC ../src/ifcgeom/Serialization/schema/Serialization.cpp)
|
||||
set_target_properties(geometry_serializer_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
target_link_libraries(geometry_serializer_ifc${schema} ${OpenCASCADE_LIBRARIES})
|
||||
target_link_libraries(geometry_serializer_ifc${schema} ${OPENCASCADE_LIBRARIES})
|
||||
list(APPEND geometry_serializer_libraries geometry_serializer_ifc${schema})
|
||||
endforeach()
|
||||
|
||||
@@ -608,14 +1070,62 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# IfcParse
|
||||
file(GLOB IFCPARSE_H_FILES_ALL ../src/ifcparse/*.h)
|
||||
file(GLOB IFCPARSE_CPP_FILES_ALL ../src/ifcparse/*.cpp)
|
||||
|
||||
foreach(file ${IFCPARSE_H_FILES_ALL})
|
||||
get_filename_component(filename "${file}" NAME)
|
||||
|
||||
if(NOT "${filename}" MATCHES "[0-9]")
|
||||
list(APPEND IFCPARSE_H_FILES "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(file ${IFCPARSE_CPP_FILES_ALL})
|
||||
get_filename_component(filename "${file}" NAME)
|
||||
|
||||
if(NOT "${filename}" MATCHES "[0-9]")
|
||||
list(APPEND IFCPARSE_CPP_FILES "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
list(APPEND IFCPARSE_H_FILES
|
||||
../src/ifcparse/Ifc${schema}.h
|
||||
../src/ifcparse/Ifc${schema}-definitions.h
|
||||
)
|
||||
list(APPEND IFCPARSE_CPP_FILES
|
||||
../src/ifcparse/Ifc${schema}.cpp
|
||||
../src/ifcparse/Ifc${schema}-schema.cpp
|
||||
)
|
||||
endforeach()
|
||||
|
||||
set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
|
||||
|
||||
add_library(IfcParse ${IFCPARSE_FILES})
|
||||
target_link_libraries(IfcParse ${STDCPPFS})
|
||||
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
|
||||
if(LibXml2_DIR)
|
||||
target_compile_definitions(IfcParse PRIVATE ${LIBXML2_DEFINITIONS})
|
||||
endif()
|
||||
if(WASM_BUILD)
|
||||
target_link_libraries(IfcParse ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
# CGAL::CGAL target already has dependencies resolved.
|
||||
if(WITH_CGAL AND CGAL_DIR)
|
||||
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)
|
||||
@@ -657,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)
|
||||
@@ -759,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
|
||||
@@ -787,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}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"IFCXML_SUPPORT": "ON",
|
||||
"HDF5_SUPPORT": "ON",
|
||||
"SCHEMA_VERSIONS": "4x3_add2",
|
||||
"CITYJSON_SUPPORT": "OFF",
|
||||
"CMAKE_GENERATOR_PLATFORM": "",
|
||||
"CMAKE_GENERATOR_TOOLSET": ""
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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})
|
||||
@@ -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})
|
||||
@@ -1,138 +0,0 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `OCC_INCLUDE_DIR`
|
||||
# - `OCC_LIBRARY_DIR`
|
||||
# If input variables are not specified, try to find OpenCASCADE config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables
|
||||
# - `OpenCASCADE_LIBRARIES`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
|
||||
|
||||
if(OCC_INCLUDE_DIR)
|
||||
set(OCC_INCLUDE_DIR ${OCC_INCLUDE_DIR} CACHE FILEPATH "Open CASCADE header files")
|
||||
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
if(OCC_LIBRARY_DIR)
|
||||
set(OCC_LIBRARY_DIR ${OCC_LIBRARY_DIR} CACHE FILEPATH "Open CASCADE library files")
|
||||
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
|
||||
if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
|
||||
# OCE is not supported for find_package, because it's using a different name (`oce`)
|
||||
# and also has an odd directory structure (install/lib/oce-0.18/*.cmake).
|
||||
# find_package creates variables:
|
||||
# - `OpenCASCADE_INCLUDE_DIR`
|
||||
# - `OpenCASCADE_LIBRARIES`
|
||||
find_package(OpenCASCADE CONFIG REQUIRED)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# No specific paths specified, try to find package.
|
||||
if(OCC_INCLUDE_DIR AND OCC_LIBRARY_DIR)
|
||||
message(
|
||||
STATUS
|
||||
"Using provided OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') "
|
||||
"and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}')."
|
||||
)
|
||||
# Parse OCC_VERSION_STRING.
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
|
||||
REGEX "#define OCC_VERSION_MAJOR.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
|
||||
REGEX "#define OCC_VERSION_MINOR.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
|
||||
REGEX "#define OCC_VERSION_MAINTENANCE.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
|
||||
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Couldn't find Open CASCADE installation. "
|
||||
"Either both OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}') "
|
||||
"must be specified or OpenCASCADE package should be discoverable. "
|
||||
"If you're using OCE, then providing a package is not available "
|
||||
"and you need to provide OCE_INCLUDE_DIR and OCE_LIBRARY_DIR directly."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(
|
||||
OpenCASCADE_LIBRARIES
|
||||
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
|
||||
TKFillet TKXSBase TKOffset TKHLR
|
||||
|
||||
# @todo investigate the exact conditions when this is necessary
|
||||
TKBin
|
||||
)
|
||||
|
||||
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OpenCASCADE_LIBRARIES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
|
||||
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OpenCASCADE_LIBRARIES TKDESTEP TKDEIGES)
|
||||
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
|
||||
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
|
||||
if(libTKernel)
|
||||
message(STATUS "Required Open Cascade Library files found")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Unable to find Open Cascade library files in OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}'), aborting"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
add_definitions(-DHAVE_NO_DLL)
|
||||
add_debug_variants(OpenCASCADE_LIBRARIES "${OpenCASCADE_LIBRARIES}" d)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# OCC might require linking to Winsock depending on the version and build configuration
|
||||
list(APPEND OpenCASCADE_LIBRARIES ws2_32.lib)
|
||||
endif()
|
||||
|
||||
# Make sure cross-referenced symbols between static OCC libraries get
|
||||
# resolved. Also add thread and rt libraries.
|
||||
get_filename_component(libTKernelExt ${libTKernel} EXT)
|
||||
if("${libTKernelExt}" STREQUAL ".a")
|
||||
set(OCCT_STATIC ON)
|
||||
endif()
|
||||
|
||||
if(OCCT_STATIC)
|
||||
find_package(Threads)
|
||||
|
||||
if(WASM_BUILD)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
# OpenCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
|
||||
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
|
||||
if(APPLE)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
set(OpenCASCADE_LIBRARIES -Wl,--start-group ${OpenCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT APPLE AND NOT WIN32)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "rt")
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "dl")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(OpenCASCADE_INTERFACE INTERFACE IMPORTED)
|
||||
target_include_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_INCLUDE_DIR}")
|
||||
target_link_libraries(OpenCASCADE_INTERFACE INTERFACE ${OpenCASCADE_LIBRARIES})
|
||||
target_link_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_LIBRARY_DIR}")
|
||||
set(OpenCASCADE_LIBRARIES OpenCASCADE_INTERFACE)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+15
-17
@@ -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.
|
||||
@@ -384,13 +382,11 @@ if MAC_CROSS_COMPILE_INTEL:
|
||||
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = ["architecture=x86"]
|
||||
MAC_CROSS_COMPILE_INTEL_CXX = "clang++ -arch x86_64"
|
||||
MAC_CROSS_COMPILE_INTEL_CC = "clang -arch x86_64"
|
||||
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = ["--host=x86_64-apple-darwin"]
|
||||
else:
|
||||
MAC_CROSS_COMPILE_INTEL_ARGS = []
|
||||
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = []
|
||||
MAC_CROSS_COMPILE_INTEL_CXX = ""
|
||||
MAC_CROSS_COMPILE_INTEL_CC = ""
|
||||
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
|
||||
|
||||
OFF_ON = ["OFF", "ON"]
|
||||
BUILD_STATIC = "shared" not in flags
|
||||
@@ -584,7 +580,7 @@ def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None,
|
||||
if "wasm" in flags:
|
||||
wasm.append("emcmake")
|
||||
|
||||
cmake_flags: list[str] = []
|
||||
cmake_flags: "list[str]" = []
|
||||
if not WASM or not WASM_CMAKE_IS_USING_INIT_VARS:
|
||||
# For WASM we provide flags using just environment variables.
|
||||
# If we provide them using cmake vars, it will override emscripten toolchain flags.
|
||||
@@ -943,9 +939,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",
|
||||
@@ -1197,9 +1195,7 @@ if "cgal" in targets:
|
||||
OLD_CC = None
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
OLD_CC = os.environ.get("CC")
|
||||
# Otherwise it's using arm64 `gcc` and fails to build gmp.
|
||||
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
|
||||
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
|
||||
|
||||
build_dependency(
|
||||
name=f"gmp-{GMP_VERSION}",
|
||||
@@ -1378,7 +1374,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 +1426,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,14 +1441,17 @@ if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServe
|
||||
|
||||
logger.info("\rBuilding executables... ")
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=executables_dir)
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], cwd=executables_dir)
|
||||
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
|
||||
|
||||
if "IfcOpenShell-Python" in targets:
|
||||
# On OSX the actual Python library is not linked against.
|
||||
ADDITIONAL_ARGS = ""
|
||||
if platform.system() == "Darwin":
|
||||
ADDITIONAL_ARGS = "-Wl,-undefined,dynamic_lookup"
|
||||
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
|
||||
|
||||
if "wasm" in flags:
|
||||
ADDITIONAL_ARGS = "-Wl,-undefined,suppress"
|
||||
|
||||
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
|
||||
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
|
||||
@@ -1486,6 +1481,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 +1500,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.
|
||||
@@ -1518,7 +1516,7 @@ if "IfcOpenShell-Python" in targets:
|
||||
|
||||
logger.info(f"\rBuilding python {python_version} wrapper... ")
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper"], cwd=python_dir)
|
||||
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
|
||||
|
||||
if python_executable:
|
||||
|
||||
@@ -41,7 +41,7 @@ def pack_dependencies(install_dir: Path) -> None:
|
||||
def unpack_dependencies(install_dir: Path) -> None:
|
||||
# `filter` argument was fully introduced in 3.12
|
||||
# and results in deprecation warnings in 3.12-3.13, if not provided.
|
||||
tar_filter: dict[Literal["filter"], Literal["data"]] = (
|
||||
tar_filter: "dict[Literal['filter'], Literal['data']]" = (
|
||||
{"filter": "data"} if bool(sys.version_info >= (3, 12)) else {}
|
||||
)
|
||||
for tar_path in install_dir.glob(f"{CACHE_PREFIX}*.tar.gz"):
|
||||
|
||||
+4
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -21,7 +21,7 @@ import bpy
|
||||
import bpy.utils.previews
|
||||
import importlib
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from . import handler, ui, prop, operator, gizmo
|
||||
from . import handler, ui, prop, operator
|
||||
from typing import Union
|
||||
from collections.abc import Callable
|
||||
|
||||
@@ -148,9 +148,7 @@ classes = [
|
||||
ui.BIM_UL_clipping_plane,
|
||||
ui.BIM_UL_generic,
|
||||
ui.DocPreferences,
|
||||
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
|
||||
ui.GizmoPreferences,
|
||||
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
|
||||
ui.BIM_ADDON_preferences,
|
||||
# Tabs panel
|
||||
ui.BIM_PT_tabs,
|
||||
# Project overview
|
||||
@@ -204,14 +202,6 @@ classes = [
|
||||
ui.BIM_PT_section_with_cappings,
|
||||
ui.BIM_PT_decorators_overlay,
|
||||
ui.BIM_PT_snappping,
|
||||
# Gizmos
|
||||
gizmo.GizmoArrow,
|
||||
gizmo.GizmoCone,
|
||||
gizmo.GizmoLock,
|
||||
gizmo.GizmoArc,
|
||||
gizmo.GizmoPen,
|
||||
gizmo.GizmoValidate,
|
||||
gizmo.GizmoCancel,
|
||||
]
|
||||
|
||||
for mod in modules.values():
|
||||
@@ -235,33 +225,14 @@ def on_register(scene):
|
||||
is_registering = False
|
||||
|
||||
|
||||
# Classes that need to be registered after modules (due to cross-module dependencies)
|
||||
late_classes = (
|
||||
ui.DefaultParameters, # Register before BIM_ADDON_preferences
|
||||
ui.BIM_ADDON_preferences,
|
||||
)
|
||||
|
||||
|
||||
def register_classes(classes_to_register):
|
||||
for cls in classes_to_register:
|
||||
def register():
|
||||
for cls in classes:
|
||||
# Prevent crashes in Blender 4.4.0, see #6420.
|
||||
if issubclass(cls, (ImportHelper, ExportHelper)):
|
||||
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
|
||||
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister_classes(classes_to_unregister):
|
||||
for cls in reversed(classes_to_unregister):
|
||||
if getattr(cls, "is_registered", None) is None:
|
||||
bpy.utils.unregister_class(cls)
|
||||
elif cls.is_registered:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
||||
def register():
|
||||
register_classes(classes)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(on_register)
|
||||
bpy.app.handlers.undo_post.append(handler.undo_post)
|
||||
bpy.app.handlers.redo_post.append(handler.redo_post)
|
||||
@@ -285,9 +256,6 @@ def register():
|
||||
for mod in modules.values():
|
||||
mod.register()
|
||||
|
||||
# Delay registering classes that depend on module classes
|
||||
register_classes(late_classes)
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
|
||||
@@ -319,7 +287,11 @@ def unregister():
|
||||
|
||||
bpy.utils.previews.remove(icons)
|
||||
|
||||
unregister_classes(classes)
|
||||
for cls in reversed(classes):
|
||||
if getattr(cls, "is_registered", None) is None:
|
||||
bpy.utils.unregister_class(cls)
|
||||
elif cls.is_registered:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
|
||||
@@ -334,9 +306,6 @@ def unregister():
|
||||
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
|
||||
bpy.types.STATUSBAR_HT_header.remove(ui.draw_statusbar)
|
||||
|
||||
# Unregister late classes before modules
|
||||
unregister_classes(late_classes)
|
||||
|
||||
for mod in reversed(list(modules.values())):
|
||||
mod.unregister()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,805 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Shared gizmo components for reuse across modules.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import math
|
||||
from typing import Optional
|
||||
from mathutils import Vector
|
||||
from mathutils.bvhtree import BVHTree
|
||||
from mathutils.geometry import intersect_line_line
|
||||
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
SNAP_CIRCLE_SEGMENTS = 16
|
||||
SNAP_CIRCLE_RADIUS = 0.1
|
||||
SNAP_CIRCLE_COLOR = (1.0, 0.5, 0.0, 1.0)
|
||||
SNAP_LINE_WIDTH = 3.0
|
||||
SNAP_MAX_RADIUS = 10.0
|
||||
|
||||
ARROW_SHAFT_LENGTH = 0.8
|
||||
ARROW_HEAD_LENGTH = 0.2
|
||||
ARROW_WIDTH = 0.015
|
||||
ARROW_HEAD_WIDTH_MULTIPLIER = 10
|
||||
ARROW_CIRCLE_SEGMENTS = 8
|
||||
|
||||
CONE_LENGTH = 1.0
|
||||
CONE_RADIUS = 0.35
|
||||
CONE_SEGMENTS = 16
|
||||
|
||||
ARC_SEGMENTS = 24
|
||||
ARC_LINE_WIDTH = 0.015
|
||||
|
||||
PRECISION_MODE_MULTIPLIER = 0.1
|
||||
|
||||
|
||||
class SnapManager:
|
||||
"""Manages snap point visualization and mesh snapping."""
|
||||
|
||||
def __init__(self):
|
||||
self._snap_point: Optional[tuple[float, float, float]] = None
|
||||
self._draw_handler = None
|
||||
self._shader = None
|
||||
|
||||
def set_snap_point(self, point: Optional[tuple[float, float, float]]) -> None:
|
||||
"""Set snap point and register draw handler if needed."""
|
||||
self._snap_point = point
|
||||
if self._draw_handler is None and point is not None:
|
||||
self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(self._draw, (), "WINDOW", "POST_VIEW")
|
||||
self._redraw_viewport()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear snap point and unregister handler."""
|
||||
self._snap_point = None
|
||||
if self._draw_handler is not None:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, "WINDOW")
|
||||
self._draw_handler = None
|
||||
self._redraw_viewport()
|
||||
|
||||
def _draw(self) -> None:
|
||||
"""Draw snap point circles."""
|
||||
if self._snap_point is None:
|
||||
return
|
||||
|
||||
if self._shader is None:
|
||||
self._shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
self._shader.bind()
|
||||
self._shader.uniform_float("color", SNAP_CIRCLE_COLOR)
|
||||
gpu.state.line_width_set(SNAP_LINE_WIDTH)
|
||||
|
||||
for plane in ("XY", "XZ", "YZ"):
|
||||
vertices = generate_circle_vertices(self._snap_point, SNAP_CIRCLE_RADIUS, SNAP_CIRCLE_SEGMENTS, plane)
|
||||
batch = batch_for_shader(self._shader, "LINE_STRIP", {"pos": vertices})
|
||||
batch.draw(self._shader)
|
||||
|
||||
gpu.state.line_width_set(1.0)
|
||||
|
||||
@staticmethod
|
||||
def _redraw_viewport() -> None:
|
||||
"""Force 3D viewport redraw."""
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
@staticmethod
|
||||
def snap_to_mesh(
|
||||
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
|
||||
) -> Vector:
|
||||
"""Snap a location to the nearest mesh element if snapping is enabled.
|
||||
|
||||
Args:
|
||||
location: The world space location to snap
|
||||
context: The Blender context
|
||||
axis_vector: The axis direction of the gizmo
|
||||
active_obj: The active object to exclude from snapping
|
||||
|
||||
Returns:
|
||||
The snapped location or original location if snapping is disabled
|
||||
"""
|
||||
tool_settings = context.scene.tool_settings
|
||||
|
||||
if not tool_settings.use_snap:
|
||||
return location
|
||||
|
||||
snap_elements = tool_settings.snap_elements_base
|
||||
|
||||
mesh_objects = [
|
||||
obj for obj in context.visible_objects if obj.type == "MESH" and obj != active_obj and obj.visible_get()
|
||||
]
|
||||
|
||||
if not mesh_objects:
|
||||
return location
|
||||
|
||||
nearby_objects = []
|
||||
for obj in mesh_objects:
|
||||
bbox_corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
|
||||
|
||||
for corner in bbox_corners:
|
||||
if (corner - location).length <= SNAP_MAX_RADIUS:
|
||||
nearby_objects.append(obj)
|
||||
break
|
||||
|
||||
if not nearby_objects:
|
||||
return location
|
||||
|
||||
closest_point = None
|
||||
closest_distance = float("inf")
|
||||
|
||||
for obj in nearby_objects:
|
||||
if not obj.data.vertices:
|
||||
continue
|
||||
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
mesh = obj_eval.to_mesh()
|
||||
|
||||
if not mesh.vertices:
|
||||
obj_eval.to_mesh_clear()
|
||||
continue
|
||||
|
||||
world_vertices = [obj.matrix_world @ v.co for v in mesh.vertices]
|
||||
|
||||
if "VERTEX" in snap_elements:
|
||||
for v_co in world_vertices:
|
||||
dist = (v_co - location).length
|
||||
if dist < closest_distance:
|
||||
closest_distance = dist
|
||||
closest_point = v_co
|
||||
|
||||
if "EDGE" in snap_elements:
|
||||
for edge in mesh.edges:
|
||||
v1 = world_vertices[edge.vertices[0]]
|
||||
v2 = world_vertices[edge.vertices[1]]
|
||||
|
||||
edge_vec = v2 - v1
|
||||
edge_len_sq = edge_vec.length_squared
|
||||
|
||||
if edge_len_sq > 0:
|
||||
t = max(0, min(1, (location - v1).dot(edge_vec) / edge_len_sq))
|
||||
closest_on_edge = v1 + t * edge_vec
|
||||
dist = (closest_on_edge - location).length
|
||||
|
||||
if dist < closest_distance:
|
||||
closest_distance = dist
|
||||
closest_point = closest_on_edge
|
||||
|
||||
if "FACE" in snap_elements:
|
||||
bvh = BVHTree.FromPolygons(world_vertices, [p.vertices for p in mesh.polygons])
|
||||
|
||||
nearest_loc, normal, index, dist = bvh.find_nearest(location)
|
||||
|
||||
if nearest_loc and dist < closest_distance:
|
||||
closest_distance = dist
|
||||
closest_point = nearest_loc
|
||||
|
||||
obj_eval.to_mesh_clear()
|
||||
|
||||
if closest_point and closest_distance < SNAP_MAX_RADIUS:
|
||||
return closest_point
|
||||
|
||||
return location
|
||||
|
||||
|
||||
_snap_manager = SnapManager()
|
||||
|
||||
|
||||
def set_snap_point(point: Optional[tuple[float, float, float]]) -> None:
|
||||
_snap_manager.set_snap_point(point)
|
||||
|
||||
|
||||
def clear_snap_point() -> None:
|
||||
_snap_manager.clear()
|
||||
|
||||
|
||||
def snap_to_mesh(
|
||||
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
|
||||
) -> Vector:
|
||||
return _snap_manager.snap_to_mesh(location, context, axis_vector, active_obj)
|
||||
|
||||
|
||||
def generate_circle_vertices(
|
||||
center: tuple[float, float, float], radius: float, segments: int, plane: str = "XY"
|
||||
) -> list[tuple[float, float, float]]:
|
||||
"""Generate circle vertices in specified plane.
|
||||
|
||||
Args:
|
||||
center: (x, y, z) tuple for circle center
|
||||
radius: Circle radius
|
||||
segments: Number of segments
|
||||
plane: 'XY', 'XZ', or 'YZ'
|
||||
|
||||
Returns:
|
||||
List of (x, y, z) vertices
|
||||
"""
|
||||
vertices = []
|
||||
for i in range(segments + 1):
|
||||
angle = (2 * math.pi * i) / segments
|
||||
cos_a = radius * math.cos(angle)
|
||||
sin_a = radius * math.sin(angle)
|
||||
|
||||
if plane == "XY":
|
||||
vertices.append((center[0] + cos_a, center[1] + sin_a, center[2]))
|
||||
elif plane == "XZ":
|
||||
vertices.append((center[0] + cos_a, center[1], center[2] + sin_a))
|
||||
else:
|
||||
vertices.append((center[0], center[1] + cos_a, center[2] + sin_a))
|
||||
|
||||
return vertices
|
||||
|
||||
|
||||
def create_quarter_circle_arc(
|
||||
radius: float = 1.0, segments: int = ARC_SEGMENTS, direction: str = "LEFT", line_width: float = ARC_LINE_WIDTH
|
||||
) -> tuple[tuple[float, float, float], ...]:
|
||||
"""Create a quarter circle arc with cross-section thickness for visibility from all angles.
|
||||
|
||||
Args:
|
||||
radius: Radius of the arc
|
||||
segments: Number of segments for smoothness
|
||||
direction: 'LEFT' for counterclockwise (0 to 90°), 'RIGHT' for clockwise (0 to -90°)
|
||||
line_width: Width of the arc line in both perpendicular directions
|
||||
|
||||
Returns:
|
||||
Tuple of arc triangles for drawing geometry visible from all angles
|
||||
"""
|
||||
half_width = line_width / 2
|
||||
|
||||
arc_points = []
|
||||
if direction == "LEFT":
|
||||
for i in range(segments + 1):
|
||||
angle = (math.pi / 2) * (i / segments)
|
||||
x = radius * math.cos(angle)
|
||||
y = radius * math.sin(angle)
|
||||
arc_points.append((x, y))
|
||||
else:
|
||||
for i in range(segments + 1):
|
||||
angle = (math.pi / 2) * (i / segments)
|
||||
x = -radius * math.cos(angle)
|
||||
y = radius * math.sin(angle)
|
||||
arc_points.append((x, y))
|
||||
|
||||
arc_triangles = []
|
||||
for i in range(len(arc_points) - 1):
|
||||
x1, y1 = arc_points[i]
|
||||
x2, y2 = arc_points[i + 1]
|
||||
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
length = (dx**2 + dy**2) ** 0.5
|
||||
if length > 0:
|
||||
px, py = -dy / length * half_width, dx / length * half_width
|
||||
|
||||
arc_triangles.extend(
|
||||
[
|
||||
(x1 + px, y1 + py, 0.0),
|
||||
(x1 - px, y1 - py, 0.0),
|
||||
(x2 + px, y2 + py, 0.0),
|
||||
]
|
||||
)
|
||||
arc_triangles.extend(
|
||||
[
|
||||
(x2 + px, y2 + py, 0.0),
|
||||
(x1 - px, y1 - py, 0.0),
|
||||
(x2 - px, y2 - py, 0.0),
|
||||
]
|
||||
)
|
||||
|
||||
arc_triangles.extend(
|
||||
[
|
||||
(x1, y1, -half_width),
|
||||
(x2, y2, -half_width),
|
||||
(x1, y1, +half_width),
|
||||
]
|
||||
)
|
||||
arc_triangles.extend(
|
||||
[
|
||||
(x1, y1, +half_width),
|
||||
(x2, y2, -half_width),
|
||||
(x2, y2, +half_width),
|
||||
]
|
||||
)
|
||||
|
||||
return tuple(arc_triangles)
|
||||
|
||||
|
||||
class GizmoMovable(bpy.types.Gizmo):
|
||||
"""Base class for movable gizmos with snapping and precision controls.
|
||||
|
||||
This class provides common functionality for gizmos that can be dragged
|
||||
to modify values, including:
|
||||
- Snap to mesh support (toggle with Ctrl)
|
||||
- Precision mode (hold Shift for 10x slower movement)
|
||||
- Header text with value and modifier hints
|
||||
- Consistent invoke/exit/modal behavior
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"custom_shape",
|
||||
"init_value",
|
||||
"move_get_cb",
|
||||
"move_set_cb",
|
||||
"axis",
|
||||
"local_axis",
|
||||
"start_location",
|
||||
"active_obj",
|
||||
"initial_snap_state",
|
||||
)
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set:
|
||||
self.init_value = self.move_get_cb() if self.move_get_cb else 0.0
|
||||
self.start_location = self.matrix_basis.translation.copy()
|
||||
self.active_obj = context.active_object
|
||||
self.initial_snap_state = context.scene.tool_settings.use_snap
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def exit(self, context: bpy.types.Context, cancel: bool) -> None:
|
||||
context.area.header_text_set(None)
|
||||
if cancel and self.move_set_cb:
|
||||
self.move_set_cb(self.init_value)
|
||||
if hasattr(self, "initial_snap_state"):
|
||||
context.scene.tool_settings.use_snap = self.initial_snap_state
|
||||
clear_snap_point()
|
||||
|
||||
def get_axis_direction(self) -> Vector:
|
||||
return self.axis
|
||||
|
||||
def modal(self, context: bpy.types.Context, event: bpy.types.Event, tweak) -> set:
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
tool_settings = context.scene.tool_settings
|
||||
|
||||
tool_settings.use_snap = not self.initial_snap_state if event.ctrl else self.initial_snap_state
|
||||
|
||||
current_coord = (event.mouse_region_x, event.mouse_region_y)
|
||||
view_origin = region_2d_to_origin_3d(region, rv3d, current_coord)
|
||||
view_direction = region_2d_to_vector_3d(region, rv3d, current_coord)
|
||||
|
||||
axis_direction = self.get_axis_direction()
|
||||
|
||||
result = intersect_line_line(
|
||||
view_origin,
|
||||
view_origin + view_direction * 1000,
|
||||
self.start_location,
|
||||
self.start_location + axis_direction * 1000,
|
||||
)
|
||||
current_3d = result[1] if result else self.start_location
|
||||
|
||||
delta = (current_3d - self.start_location).dot(axis_direction)
|
||||
|
||||
if tool_settings.use_snap and self.active_obj:
|
||||
snapped_pos = snap_to_mesh(current_3d, context, axis_direction, self.active_obj)
|
||||
if snapped_pos != current_3d:
|
||||
delta = (snapped_pos - self.start_location).dot(axis_direction)
|
||||
set_snap_point(snapped_pos)
|
||||
else:
|
||||
clear_snap_point()
|
||||
else:
|
||||
clear_snap_point()
|
||||
|
||||
if event.shift:
|
||||
delta *= PRECISION_MODE_MULTIPLIER
|
||||
|
||||
if self.move_set_cb:
|
||||
self.move_set_cb(self.init_value + delta)
|
||||
|
||||
self._update_header(context, self.init_value + delta, tool_settings.use_snap, event.shift)
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _update_header(self, context: bpy.types.Context, value: float, is_snapping: bool, is_precision: bool) -> None:
|
||||
header_text = f"Value: {value:.3f}m"
|
||||
hints = []
|
||||
if is_snapping:
|
||||
hints.append("Snapping: ON")
|
||||
if is_precision:
|
||||
hints.append("Precision Mode (0.1x)")
|
||||
hints.extend(["Ctrl: Toggle Snap", "Shift: Precision"])
|
||||
|
||||
if hints:
|
||||
header_text += " | " + " | ".join(hints)
|
||||
context.area.header_text_set(header_text)
|
||||
|
||||
|
||||
class GizmoLock(bpy.types.Gizmo):
|
||||
"""Reusable lock icon gizmo that switches between closed and open states."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_lock"
|
||||
|
||||
__slots__ = (
|
||||
"custom_shape_closed",
|
||||
"custom_shape_open",
|
||||
"prop_path",
|
||||
)
|
||||
|
||||
tris_closed = (
|
||||
(0.16803650558, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(0.20165449381, 0.13603900373, 0.0),
|
||||
(-0.11167433113, 0.13603900373, 0.0),
|
||||
(0.20165449381, -0.44701099396, 0.0),
|
||||
(0.20165449381, -0.44701099396, 0.0),
|
||||
(-0.11167433113, 0.13603900373, 0.0),
|
||||
(-0.11167433113, -0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(-0.39353451133, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.39353451133, 0.18791499734, 0.0),
|
||||
(-0.39353451133, 0.30746498704, 0.0),
|
||||
(-0.44701099396, 0.18791499734, 0.0),
|
||||
(-0.44701099396, -0.04477182776, 0.0),
|
||||
(-0.39353451133, 0.18791499734, 0.0),
|
||||
(-0.39353451133, 0.18791499734, 0.0),
|
||||
(-0.44701099396, -0.04477182776, 0.0),
|
||||
(-0.39353451133, -0.04477182776, 0.0),
|
||||
)
|
||||
|
||||
tris_open = (
|
||||
(0.16803650558, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(0.20165449381, 0.13603900373, 0.0),
|
||||
(-0.11167433113, 0.13603900373, 0.0),
|
||||
(0.20165449381, -0.44701099396, 0.0),
|
||||
(0.20165449381, -0.44701099396, 0.0),
|
||||
(-0.11167433113, 0.13603900373, 0.0),
|
||||
(-0.11167433113, -0.44701099396, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(0.16803650558, 0.70610702038, 0.0),
|
||||
(0.16803650558, 0.70610702038, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.58656096458, 0.0),
|
||||
(-0.11167433113, 0.44701099396, 0.0),
|
||||
(-0.11167433113, 0.73432201147, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.11167433113, 0.73432201147, 0.0),
|
||||
(-0.07805634290, 0.73432201147, 0.0),
|
||||
)
|
||||
|
||||
def get_custom_shape(self, context: bpy.types.Context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return self.custom_shape_closed
|
||||
|
||||
try:
|
||||
is_open = obj.path_resolve(self.prop_path)
|
||||
return self.custom_shape_open if is_open else self.custom_shape_closed
|
||||
except:
|
||||
return self.custom_shape_closed
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape_closed = self.new_custom_shape("TRIS", self.tris_closed)
|
||||
self.custom_shape_open = self.new_custom_shape("TRIS", self.tris_open)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.get_custom_shape(context))
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.get_custom_shape(context), select_id=select_id)
|
||||
|
||||
|
||||
class GizmoArc(bpy.types.Gizmo):
|
||||
"""Reusable quarter circle arc gizmo."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_arc"
|
||||
|
||||
__slots__ = (
|
||||
"custom_shape_left",
|
||||
"custom_shape_right",
|
||||
"prop_path",
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Create quarter circle shapes for both LEFT and RIGHT directions."""
|
||||
arc_left = create_quarter_circle_arc(radius=1.0, direction="LEFT")
|
||||
arc_right = create_quarter_circle_arc(radius=1.0, direction="RIGHT")
|
||||
|
||||
self.custom_shape_left = self.new_custom_shape(type="TRIS", verts=arc_left)
|
||||
self.custom_shape_right = self.new_custom_shape(type="TRIS", verts=arc_right)
|
||||
|
||||
def _get_shape_for_direction(self, context: bpy.types.Context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return self.custom_shape_left
|
||||
|
||||
try:
|
||||
direction_value = obj.path_resolve(self.prop_path)
|
||||
if "RIGHT" in str(direction_value):
|
||||
return self.custom_shape_right
|
||||
except:
|
||||
pass
|
||||
|
||||
return self.custom_shape_left
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self._get_shape_for_direction(context))
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self._get_shape_for_direction(context), select_id=select_id)
|
||||
|
||||
|
||||
class GizmoPen(bpy.types.Gizmo):
|
||||
"""Reusable pen/edit icon gizmo for entering edit mode."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_pen"
|
||||
|
||||
__slots__ = ("custom_shape",)
|
||||
|
||||
tris = (
|
||||
(-0.07595771551132202, -0.2948460578918457, 0.0),
|
||||
(0.16886109113693237, 0.23203276097774506, 0.0),
|
||||
(0.062240585684776306, 0.28157487511634827, 0.0),
|
||||
(0.07201281189918518, 0.30260589718818665, 0.0),
|
||||
(0.21042980253696442, 0.321493536233902, 0.0),
|
||||
(0.17863331735134125, 0.25306373834609985, 0.0),
|
||||
(0.062240585684776306, 0.28157487511634827, 0.0),
|
||||
(-0.1825782209634781, -0.2453039139509201, 0.0),
|
||||
(-0.07595771551132202, -0.2948460578918457, 0.0),
|
||||
(-0.1825782209634781, -0.2453039139509201, 0.0),
|
||||
(-0.19114767014980316, -0.4032624065876007, 0.0),
|
||||
(-0.07595771551132202, -0.2948460578918457, 0.0),
|
||||
(0.07201281189918518, 0.30260589718818665, 0.0),
|
||||
(0.10380929708480835, 0.371035635471344, 0.0),
|
||||
(0.21042980253696442, 0.321493536233902, 0.0),
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoValidate(bpy.types.Gizmo):
|
||||
"""Reusable validate/checkmark icon gizmo for confirming edits."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_validate"
|
||||
|
||||
__slots__ = ("custom_shape",)
|
||||
|
||||
tris = (
|
||||
(0.36775994300842285, 0.205583393573761, 0.0),
|
||||
(0.030080009251832962, -0.1881658434867859, 0.0),
|
||||
(0.030080009251832962, -0.3380376696586609, 0.0),
|
||||
(-0.22017201781272888, -0.16090886294841766, 0.0),
|
||||
(0.030080009251832962, -0.1881658434867859, 0.0),
|
||||
(-0.22017201781272888, -0.011037036776542664, 0.0),
|
||||
(0.36775994300842285, 0.205583393573761, 0.0),
|
||||
(0.36775994300842285, 0.355455219745636, 0.0),
|
||||
(0.030080009251832962, -0.1881658434867859, 0.0),
|
||||
(-0.22017201781272888, -0.16090886294841766, 0.0),
|
||||
(0.030080009251832962, -0.3380376696586609, 0.0),
|
||||
(0.030080009251832962, -0.1881658434867859, 0.0),
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoCancel(bpy.types.Gizmo):
|
||||
"""Reusable cancel/X icon gizmo for canceling edits."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_cancel"
|
||||
|
||||
__slots__ = ("custom_shape",)
|
||||
|
||||
tris = (
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(0.0, -0.048707593232393265, 0.0),
|
||||
(0.048707593232393265, 0.0, 0.0),
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(-0.21918421983718872, 0.2678918242454529, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(-0.21918421983718872, 0.2678918242454529, 0.0),
|
||||
(-0.2678918242454529, 0.21918421983718872, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(-0.2678918242454529, -0.21918421983718872, 0.0),
|
||||
(-0.21918421983718872, -0.2678918242454529, 0.0),
|
||||
(0.2678918242454529, 0.21918421983718872, 0.0),
|
||||
(0.21918421983718872, 0.2678918242454529, 0.0),
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(0.21918421983718872, -0.2678918242454529, 0.0),
|
||||
(0.2678918242454529, -0.21918421983718872, 0.0),
|
||||
(0.048707593232393265, 0.0, 0.0),
|
||||
(0.048707593232393265, 0.0, 0.0),
|
||||
(0.2678918242454529, 0.21918421983718872, 0.0),
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(0.0, -0.048707593232393265, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(-0.21918421983718872, -0.2678918242454529, 0.0),
|
||||
(0.0, -0.048707593232393265, 0.0),
|
||||
(0.0, -0.048707593232393265, 0.0),
|
||||
(0.21918421983718872, -0.2678918242454529, 0.0),
|
||||
(0.048707593232393265, 0.0, 0.0),
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoArrow(GizmoMovable):
|
||||
"""Arrow gizmo for directional value editing."""
|
||||
|
||||
bl_idname = "BIM_GT_gizmo_arrow"
|
||||
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
|
||||
|
||||
def _get_arrow_triangles(self) -> tuple[tuple[float, float, float], ...]:
|
||||
"""Generate arrow geometry along +X axis."""
|
||||
triangles = []
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(0, -ARROW_WIDTH, 0),
|
||||
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
|
||||
(0, +ARROW_WIDTH, 0),
|
||||
]
|
||||
)
|
||||
triangles.extend(
|
||||
[
|
||||
(0, +ARROW_WIDTH, 0),
|
||||
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
|
||||
(ARROW_SHAFT_LENGTH, +ARROW_WIDTH, 0),
|
||||
]
|
||||
)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(0, 0, -ARROW_WIDTH),
|
||||
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
|
||||
(0, 0, +ARROW_WIDTH),
|
||||
]
|
||||
)
|
||||
triangles.extend(
|
||||
[
|
||||
(0, 0, +ARROW_WIDTH),
|
||||
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
|
||||
(ARROW_SHAFT_LENGTH, 0, +ARROW_WIDTH),
|
||||
]
|
||||
)
|
||||
|
||||
head_width = ARROW_WIDTH * ARROW_HEAD_WIDTH_MULTIPLIER
|
||||
triangles.extend(
|
||||
[
|
||||
(ARROW_SHAFT_LENGTH, -head_width, 0),
|
||||
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
|
||||
(ARROW_SHAFT_LENGTH, +head_width, 0),
|
||||
]
|
||||
)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(ARROW_SHAFT_LENGTH, 0, -head_width),
|
||||
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
|
||||
(ARROW_SHAFT_LENGTH, 0, +head_width),
|
||||
]
|
||||
)
|
||||
|
||||
for i in range(ARROW_CIRCLE_SEGMENTS):
|
||||
angle1 = (2 * math.pi * i) / ARROW_CIRCLE_SEGMENTS
|
||||
angle2 = (2 * math.pi * (i + 1)) / ARROW_CIRCLE_SEGMENTS
|
||||
|
||||
y1 = head_width * math.cos(angle1)
|
||||
z1 = head_width * math.sin(angle1)
|
||||
y2 = head_width * math.cos(angle2)
|
||||
z2 = head_width * math.sin(angle2)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(ARROW_SHAFT_LENGTH, 0, 0),
|
||||
(ARROW_SHAFT_LENGTH, y1, z1),
|
||||
(ARROW_SHAFT_LENGTH, y2, z2),
|
||||
]
|
||||
)
|
||||
|
||||
return tuple(triangles)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self._get_arrow_triangles())
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoCone(GizmoMovable):
|
||||
"""Cone gizmo for directional value editing with local axis support."""
|
||||
|
||||
bl_idname = "BIM_GT_gizmo_cone"
|
||||
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
|
||||
|
||||
def get_axis_direction(self) -> Vector:
|
||||
if hasattr(self, "local_axis") and self.active_obj:
|
||||
obj_rotation = self.active_obj.matrix_world.to_3x3()
|
||||
axis_direction = obj_rotation @ self.local_axis
|
||||
axis_direction.normalize()
|
||||
return axis_direction
|
||||
return self.axis
|
||||
|
||||
def _get_cone_triangles(self) -> tuple[tuple[float, float, float], ...]:
|
||||
"""Generate cone geometry along +X axis."""
|
||||
triangles = []
|
||||
cone_tip_x = CONE_LENGTH
|
||||
|
||||
for i in range(CONE_SEGMENTS):
|
||||
angle1 = (2 * math.pi * i) / CONE_SEGMENTS
|
||||
angle2 = (2 * math.pi * (i + 1)) / CONE_SEGMENTS
|
||||
|
||||
y1 = CONE_RADIUS * math.cos(angle1)
|
||||
z1 = CONE_RADIUS * math.sin(angle1)
|
||||
y2 = CONE_RADIUS * math.cos(angle2)
|
||||
z2 = CONE_RADIUS * math.sin(angle2)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(cone_tip_x, 0, 0),
|
||||
(0, y1, z1),
|
||||
(0, y2, z2),
|
||||
]
|
||||
)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(0, 0, 0),
|
||||
(0, y2, z2),
|
||||
(0, y1, z1),
|
||||
]
|
||||
)
|
||||
|
||||
return tuple(triangles)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self._get_cone_triangles())
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
# Liang–Barsky 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 (
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
@@ -2549,264 +2486,6 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_changing_mode = False
|
||||
|
||||
|
||||
class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.direct_profile_edit"
|
||||
bl_label = "IFC Direct Profile Edit"
|
||||
bl_description = "Directly enter profile/axis edit mode, or exit back to object mode"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Only allow in OBJECT or EDIT modes
|
||||
if context.mode not in ("OBJECT", "EDIT_MESH", "EDIT_CURVE"):
|
||||
return False
|
||||
|
||||
# Must have an active object
|
||||
if not context.active_object:
|
||||
return False
|
||||
|
||||
# In edit mode, we're good to go (we'll exit)
|
||||
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
|
||||
return True
|
||||
|
||||
# In object mode, check if it's an editable object type
|
||||
obj = context.active_object
|
||||
|
||||
# Only mesh and curve objects are editable
|
||||
if obj.type not in ("MESH", "CURVE"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
# Ensure we're only working with the active object
|
||||
# Deselect all other objects if multiple are selected
|
||||
active_obj = context.active_object
|
||||
if active_obj and context.mode == "OBJECT":
|
||||
if len(context.selected_objects) > 1:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Currently Direct Profile Edit only works on one object at a time. Operating on active object only.",
|
||||
)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
active_obj.select_set(True)
|
||||
context.view_layer.objects.active = active_obj
|
||||
|
||||
# If we're already in edit mode, exit to object mode and SAVE changes
|
||||
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
|
||||
return self.handle_exit_edit_mode(context)
|
||||
|
||||
# We're in object mode, try to enter profile/axis edit
|
||||
return self.handle_enter_edit_mode(context)
|
||||
|
||||
def handle_exit_edit_mode(self, context):
|
||||
"""Handle exiting from edit mode and saving changes."""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if we're editing a representation item
|
||||
if tool.Geometry.is_representation_item(obj):
|
||||
return self.exit_item_edit_mode(context, obj)
|
||||
|
||||
# Check if we're editing an element-level profile or axis
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and tool.Geometry.has_mesh_properties(obj.data):
|
||||
return self.exit_element_edit_mode(context, obj, element)
|
||||
|
||||
# For other edit modes, use standard operator
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
def exit_item_edit_mode(self, context, obj):
|
||||
"""Exit from SweptAreaSolid item editing."""
|
||||
try:
|
||||
item = tool.Geometry.get_active_representation(obj)
|
||||
if not item or not item.is_a("IfcSweptAreaSolid"):
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
# Use the standard save workflow for items
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
ProfileDecorator.uninstall()
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
|
||||
profile = tool.Model.export_profile(obj)
|
||||
if not profile:
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE")
|
||||
|
||||
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
ProfileDecorator.install(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
old_profile = item.SweptArea
|
||||
profile.ProfileName = old_profile.ProfileName
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
for inverse in ifc_file.get_inverse(old_profile):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
|
||||
|
||||
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, old_profile)
|
||||
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
if props.representation_obj:
|
||||
tool.Geometry.reload_representation(props.representation_obj)
|
||||
tool.Geometry.disable_item_mode()
|
||||
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to save item changes: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def exit_element_edit_mode(self, context, obj, element):
|
||||
"""Exit from element-level profile or axis editing."""
|
||||
try:
|
||||
mesh_props = tool.Geometry.get_mesh_props(obj.data)
|
||||
|
||||
# Check if we're editing axis
|
||||
if mesh_props.subshape_type == "AXIS":
|
||||
return bpy.ops.bim.edit_extrusion_axis()
|
||||
|
||||
# Otherwise, we're editing a profile
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if extrusion:
|
||||
return bpy.ops.bim.edit_extrusion_profile()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to save profile/axis changes: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Fallback to standard operator
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
def handle_enter_edit_mode(self, context):
|
||||
"""Handle entering edit mode from object mode."""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if object has mesh or curve data
|
||||
if not hasattr(obj, "data") or not obj.data:
|
||||
self.report({"INFO"}, "Object has no editable data")
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
self.report({"INFO"}, "Active object is not an IFC element")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if this is a LAYER2 element (wall, railing, etc.)
|
||||
try:
|
||||
material_usage = tool.Model.get_usage_type(element)
|
||||
except:
|
||||
material_usage = None
|
||||
|
||||
if material_usage == "LAYER2":
|
||||
self.report({"ERROR"}, "LAYER2 elements (walls, railings, etc.) cannot use profile editing.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Try to get representation
|
||||
try:
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
except:
|
||||
representation = None
|
||||
|
||||
if not representation:
|
||||
self.report({"INFO"}, "Object has no active representation")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check for PROFILE usage (beams, columns, members) - should edit axis
|
||||
if material_usage == "PROFILE":
|
||||
try:
|
||||
return bpy.ops.bim.enable_editing_extrusion_axis()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enable axis editing: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if this is an element with an extrusion profile (LAYER3, etc)
|
||||
try:
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if extrusion and not tool.Geometry.is_representation_item(obj):
|
||||
return bpy.ops.bim.enable_editing_extrusion_profile()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enable profile editing: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Otherwise, try to enter item mode and edit SweptAreaSolid
|
||||
return self.enter_item_edit_mode(context, obj)
|
||||
|
||||
def enter_item_edit_mode(self, context, obj):
|
||||
"""Enter item mode for SweptAreaSolid editing."""
|
||||
try:
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
if not props.representation_obj:
|
||||
bpy.ops.bim.import_representation_items()
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
|
||||
# Find the SweptAreaSolid item
|
||||
swept_solid_obj = None
|
||||
for item_obj_data in props.item_objs:
|
||||
item_obj = item_obj_data.obj
|
||||
if not item_obj:
|
||||
continue
|
||||
try:
|
||||
item = tool.Geometry.get_active_representation(item_obj)
|
||||
if item and item.is_a("IfcSweptAreaSolid"):
|
||||
swept_solid_obj = item_obj
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not swept_solid_obj:
|
||||
self.report({"INFO"}, "No editable SweptAreaSolid item found")
|
||||
tool.Geometry.disable_item_mode()
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Select and activate the swept solid object
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
swept_solid_obj.select_set(True)
|
||||
context.view_layer.objects.active = swept_solid_obj
|
||||
|
||||
# Import the profile to prepare for editing
|
||||
item = tool.Geometry.get_active_representation(swept_solid_obj)
|
||||
if item and item.is_a("IfcSweptAreaSolid"):
|
||||
profile = item.SweptArea
|
||||
res = tool.Model.import_profile(profile, obj=swept_solid_obj)
|
||||
if res is None:
|
||||
self.report({"ERROR"}, "Couldn't import profile for editing")
|
||||
tool.Geometry.disable_item_mode()
|
||||
return {"CANCELLED"}
|
||||
|
||||
tool.Ifc.link(item, swept_solid_obj.data)
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
ProfileDecorator.install(context)
|
||||
|
||||
if not bpy.app.background:
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enter item edit mode: {str(e)}")
|
||||
try:
|
||||
tool.Geometry.disable_item_mode()
|
||||
except:
|
||||
pass
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class FlipObject(bpy.types.Operator):
|
||||
bl_idname = "bim.flip_object"
|
||||
bl_label = "Flip Object"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -178,8 +178,6 @@ classes = (
|
||||
door.FinishEditingDoor,
|
||||
door.EnableEditingDoor,
|
||||
door.RemoveDoor,
|
||||
door.ToggleDoorSwing,
|
||||
door.GizmoDoorEdition,
|
||||
railing.BIM_OT_add_railing,
|
||||
railing.CopyRailingParameters,
|
||||
railing.AddRailing,
|
||||
|
||||
@@ -34,7 +34,6 @@ import bonsai.core.geometry as core
|
||||
import bonsai.core.root
|
||||
from bonsai.bim.module.model.window import create_bm_window, create_bm_box
|
||||
|
||||
import math
|
||||
from mathutils import Vector, Matrix
|
||||
from typing import Union, Any, Optional
|
||||
|
||||
@@ -279,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)
|
||||
|
||||
@@ -292,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
|
||||
@@ -411,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(
|
||||
@@ -484,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.")
|
||||
@@ -534,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)
|
||||
@@ -554,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
|
||||
@@ -565,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:
|
||||
@@ -600,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):
|
||||
@@ -637,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):
|
||||
@@ -681,321 +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 between LEFT and RIGHT"""
|
||||
|
||||
bl_idname = "bim.toggle_door_swing"
|
||||
bl_label = "Toggle Door Swing"
|
||||
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_door(element):
|
||||
return {"CANCELLED"}
|
||||
|
||||
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")
|
||||
elif "RIGHT" in current_type:
|
||||
props.door_type = current_type.replace("RIGHT", "LEFT")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GizmoDoorEdition(bpy.types.GizmoGroup):
|
||||
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"}
|
||||
|
||||
COLOR_RED = (1.0, 0.2, 0.2)
|
||||
COLOR_GREEN = (0.1, 0.8, 0.1)
|
||||
COLOR_BLUE = (0.2, 0.2, 1.0)
|
||||
ARROW_SCALE = 0.25
|
||||
|
||||
@classmethod
|
||||
def get_arrow_color_from_axis(cls, axis):
|
||||
"""Get arrow color based on axis direction (X=red, Y=green, Z=blue)."""
|
||||
if axis[0] != 0:
|
||||
return cls.COLOR_RED
|
||||
elif axis[1] != 0:
|
||||
return cls.COLOR_GREEN
|
||||
else:
|
||||
return cls.COLOR_BLUE
|
||||
|
||||
gizmo_props = [
|
||||
{"attr_name": "overall_height", "axis": (0, 0, 1)},
|
||||
{"attr_name": "overall_width", "axis": (1, 0, 0)},
|
||||
{"attr_name": "threshold_thickness", "axis": (0, 0, 1)},
|
||||
{"attr_name": "threshold_depth", "axis": (0, 1, 0)},
|
||||
{"attr_name": "lining_depth", "axis": (0, 1, 0)},
|
||||
{"attr_name": "lining_thickness", "axis": (1, 0, 0)},
|
||||
{"attr_name": "transom_offset", "axis": (0, 0, 1)},
|
||||
{"attr_name": "transom_thickness", "axis": (0, 0, 1)},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
"""Show gizmo only when a single door object is selected and active."""
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if not prefs.gizmos.draw_gizmos_in_3d_viewport:
|
||||
return False
|
||||
|
||||
obj = tool.Blender.get_active_object(is_selected=True)
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
if len(tool.Blender.get_selected_objects()) != 1:
|
||||
return False
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Blender.Modifier.is_door(element):
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_axis_rotation_matrix(self, axis):
|
||||
"""Get rotation matrix to align arrow (default +X direction) with the given axis.
|
||||
|
||||
Args:
|
||||
axis: Tuple of (x, y, z) representing the target axis direction
|
||||
|
||||
Returns:
|
||||
Rotation matrix to align arrow with the axis
|
||||
"""
|
||||
axis_vec = V_(*axis).normalized()
|
||||
default_dir = V_(1, 0, 0)
|
||||
return default_dir.rotation_difference(axis_vec).to_matrix().to_4x4()
|
||||
|
||||
def get_gizmo_matrix_overall_height(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width - 0.05, 0.0, 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, 0.0, 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.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.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.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
|
||||
|
||||
def get_gizmo_matrix_transom_offset(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width / 2, 0.0, props.transom_offset))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_transom_thickness(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, 0.0, props.transom_offset + props.transom_thickness)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def setup(self, context):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
default_color = prefs.decorations_colour[:3]
|
||||
highlight_color = prefs.decorator_color_selected[:3]
|
||||
inactive_color = prefs.decorator_color_background[:3]
|
||||
special_color = prefs.decorator_color_special[:3]
|
||||
|
||||
def add_gizmo_prop(prop_config):
|
||||
attr_name = prop_config["attr_name"]
|
||||
gizmo = self.gizmos.new("BIM_GT_gizmo_cone")
|
||||
|
||||
def move_get():
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return 0.0
|
||||
props = tool.Model.get_door_props(obj)
|
||||
return getattr(props, attr_name)
|
||||
|
||||
def move_set(value):
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
props = tool.Model.get_door_props(obj)
|
||||
setattr(props, attr_name, max(0.0, value))
|
||||
|
||||
# Set callbacks on the gizmo instance
|
||||
gizmo.move_get_cb = move_get
|
||||
gizmo.move_set_cb = move_set
|
||||
gizmo.axis = V_(*prop_config["axis"])
|
||||
gizmo.local_axis = V_(*prop_config["axis"])
|
||||
|
||||
gizmo.color = self.get_arrow_color_from_axis(prop_config["axis"])
|
||||
gizmo.color_highlight = highlight_color
|
||||
gizmo.alpha = 0.99
|
||||
gizmo.use_draw_modal = True
|
||||
gizmo.use_draw_scale = True
|
||||
|
||||
setattr(self, f"gizmo_{attr_name}", gizmo)
|
||||
|
||||
for prop_config in self.gizmo_props:
|
||||
add_gizmo_prop(prop_config)
|
||||
|
||||
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"
|
||||
self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
|
||||
|
||||
# Flip arc gizmo - mirrors the swing arc along X axis, flips door 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.flip_object")
|
||||
op.flip_local_axes = "XY"
|
||||
|
||||
self.pen_gizmo = self.gizmos.new("VIEW3D_GT_pen")
|
||||
self.pen_gizmo.use_draw_scale = False
|
||||
self.pen_gizmo.color = default_color
|
||||
self.pen_gizmo.color_highlight = highlight_color
|
||||
self.pen_gizmo.alpha = 0.8
|
||||
self.pen_gizmo.target_set_operator("bim.enable_editing_door")
|
||||
|
||||
self.validate_gizmo = self.gizmos.new("VIEW3D_GT_validate")
|
||||
self.validate_gizmo.use_draw_scale = False
|
||||
self.validate_gizmo.color = self.COLOR_GREEN
|
||||
self.validate_gizmo.color_highlight = highlight_color
|
||||
self.validate_gizmo.alpha = 0.8
|
||||
self.validate_gizmo.target_set_operator("bim.finish_editing_door")
|
||||
|
||||
self.cancel_gizmo = self.gizmos.new("VIEW3D_GT_cancel")
|
||||
self.cancel_gizmo.use_draw_scale = False
|
||||
self.cancel_gizmo.color = self.COLOR_RED
|
||||
self.cancel_gizmo.color_highlight = highlight_color
|
||||
self.cancel_gizmo.alpha = 0.8
|
||||
self.cancel_gizmo.target_set_operator("bim.cancel_editing_door")
|
||||
|
||||
def refresh(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
props = tool.Model.get_door_props(obj)
|
||||
mw = obj.matrix_world
|
||||
self.update_arrows(mw, props)
|
||||
self.update_swing_gizmo(mw, props)
|
||||
self.update_editing_gizmos(mw, props)
|
||||
|
||||
def update_arrows(self, mw, props):
|
||||
"""Update arrow gizmos position and visibility based on editing state."""
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
door_gizmo_prefs = prefs.gizmos.door
|
||||
|
||||
for prop_config in self.gizmo_props:
|
||||
attr_name = prop_config["attr_name"]
|
||||
gizmo = getattr(self, f"gizmo_{attr_name}", None)
|
||||
if gizmo is None:
|
||||
continue
|
||||
|
||||
if not getattr(door_gizmo_prefs, attr_name, True):
|
||||
gizmo.hide = True
|
||||
continue
|
||||
|
||||
if (
|
||||
not props.is_editing
|
||||
or (attr_name == "threshold_depth" and props.threshold_thickness == 0.0)
|
||||
or (attr_name == "transom_offset" and props.transom_thickness == 0.0)
|
||||
):
|
||||
gizmo.hide = True
|
||||
continue
|
||||
|
||||
# Update position and show arrow when editing
|
||||
gizmo.hide = False
|
||||
matrix_method = getattr(self, f"get_gizmo_matrix_{attr_name}", None)
|
||||
if matrix_method:
|
||||
gizmo.matrix_basis = mw @ matrix_method(props)
|
||||
gizmo.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
|
||||
|
||||
def update_swing_gizmo(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, 0, 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_x = Matrix.Scale(-1, 4, (0, 1, 0))
|
||||
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_x
|
||||
|
||||
def update_editing_gizmos(self, mw, props):
|
||||
"""Update editing control gizmos (pen/validate/cancel) visibility and position."""
|
||||
|
||||
icon_z = props.overall_height + 0.5
|
||||
local_transform = (
|
||||
Matrix.Translation(V_(0, 0.0, icon_z))
|
||||
@ Matrix.Rotation(math.radians(90), 4, (1.0, 0.0, 0.0))
|
||||
@ Matrix.Scale(0.5, 4)
|
||||
)
|
||||
icon_matrix_base = mw @ local_transform
|
||||
|
||||
if props.is_editing:
|
||||
self.pen_gizmo.hide = True
|
||||
self.validate_gizmo.hide = False
|
||||
self.validate_gizmo.matrix_basis = icon_matrix_base
|
||||
self.cancel_gizmo.hide = False
|
||||
cancel_local = Matrix.Translation(V_(0.5, 0.0, 0.0)) @ local_transform
|
||||
self.cancel_gizmo.matrix_basis = mw @ cancel_local
|
||||
|
||||
else:
|
||||
self.pen_gizmo.hide = False
|
||||
self.pen_gizmo.matrix_basis = icon_matrix_base
|
||||
self.validate_gizmo.hide = True
|
||||
self.cancel_gizmo.hide = True
|
||||
|
||||
@@ -348,136 +348,24 @@ class BIMArrayProperties(PropertyGroup):
|
||||
|
||||
|
||||
def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update tread_run when total_length_target changes"""
|
||||
# Calculate available length for default treads
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1 # number of risers
|
||||
|
||||
# Subtract custom first tread if not locked and not zero
|
||||
if not self.custom_tread_lock and self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
|
||||
# Subtract custom last tread if not locked and not zero
|
||||
if not self.custom_tread_lock and self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
# Calculate tread_run for remaining treads
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
# All treads are custom, just use target length
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
|
||||
|
||||
def update_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update either number_of_treads or total_length_target when tread_run changes"""
|
||||
if self.total_length_lock:
|
||||
# Calculate how much length custom treads take up
|
||||
custom_length = 0
|
||||
n_custom_treads = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[0]
|
||||
n_custom_treads += 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[1]
|
||||
n_custom_treads += 1
|
||||
|
||||
# Calculate how many default treads fit in remaining space
|
||||
available_length = self.total_length_target - custom_length
|
||||
if self.tread_run > 0:
|
||||
n_default_treads = available_length / self.tread_run
|
||||
total_treads = n_default_treads + n_custom_treads
|
||||
# number_of_treads = number_of_risers - 1
|
||||
self["number_of_treads"] = int(total_treads - 1)
|
||||
self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1)
|
||||
else:
|
||||
# Calculate total length from current settings
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
|
||||
|
||||
def update_number_of_treads(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update either tread_run or total_length_target when number_of_treads changes"""
|
||||
if self.total_length_lock:
|
||||
# Calculate available length for default treads
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
else:
|
||||
# Calculate total length from current settings
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
|
||||
|
||||
def update_custom_first_last_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update tread_run or total_length when custom treads change"""
|
||||
if self.total_length_lock:
|
||||
# Recalculate tread_run to maintain total length
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
# Recalculate total length
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"]
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
@@ -520,7 +408,7 @@ class BIMStairProperties(PropertyGroup):
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
|
||||
stair_type: bpy.props.EnumProperty(
|
||||
name="Stair Type",
|
||||
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(tool.Model.StairType)],
|
||||
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(StairType)],
|
||||
default="CONCRETE",
|
||||
update=validate_nosing_value,
|
||||
)
|
||||
@@ -537,7 +425,6 @@ class BIMStairProperties(PropertyGroup):
|
||||
min=0,
|
||||
unit="LENGTH",
|
||||
size=2,
|
||||
update=update_custom_first_last_tread_run, # Added update callback
|
||||
)
|
||||
nosing_length: bpy.props.FloatProperty(
|
||||
name="Nosing Length",
|
||||
@@ -605,18 +492,16 @@ class BIMStairProperties(PropertyGroup):
|
||||
}
|
||||
stair_kwargs.update(generic_props)
|
||||
|
||||
non_si_units_props = self.non_si_units_props
|
||||
# If locked, use tread_run for both first and last treads
|
||||
if self.custom_tread_lock:
|
||||
non_si_units_props += ("custom_first_last_tread_run",)
|
||||
stair_kwargs["custom_first_last_tread_run"] = (None, None)
|
||||
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||
else:
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
|
||||
if not convert_to_project_units:
|
||||
return stair_kwargs
|
||||
|
||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, non_si_units_props)
|
||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
|
||||
return stair_kwargs
|
||||
|
||||
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
|
||||
@@ -628,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)
|
||||
@@ -872,14 +743,6 @@ 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",
|
||||
@@ -909,35 +772,13 @@ class BIMDoorProperties(PropertyGroup):
|
||||
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",
|
||||
@@ -959,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,
|
||||
)
|
||||
@@ -967,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,
|
||||
)
|
||||
@@ -976,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",
|
||||
@@ -1012,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",
|
||||
@@ -1028,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
|
||||
@@ -1147,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"]
|
||||
@@ -1254,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)
|
||||
@@ -1361,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:
|
||||
|
||||
@@ -131,7 +131,7 @@ def update_ifc_stair_props(obj: bpy.types.Object) -> None:
|
||||
|
||||
class BIM_OT_add_stair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_stair"
|
||||
bl_label = "Add Stair"
|
||||
bl_label = "Stair"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
@@ -188,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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -258,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 = []
|
||||
@@ -402,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
|
||||
@@ -454,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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
-177
@@ -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,48 +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)
|
||||
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
|
||||
swing_arc: bool
|
||||
flip_arc: 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)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
draw_gizmos_in_3d_viewport: bool
|
||||
door: GizmoPreferencesDoor
|
||||
|
||||
|
||||
class DocPreferences(bpy.types.PropertyGroup):
|
||||
sheets_dir: StringProperty(
|
||||
default=os.path.join("sheets") + os.path.sep,
|
||||
@@ -376,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,
|
||||
@@ -441,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",
|
||||
@@ -462,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(
|
||||
@@ -526,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(
|
||||
@@ -557,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
|
||||
@@ -588,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
|
||||
@@ -605,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]
|
||||
@@ -623,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
|
||||
@@ -671,34 +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:
|
||||
box = layout.box()
|
||||
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Door", self.draw_door_gizmo_parameters)
|
||||
|
||||
def draw_door_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
door_gizmos = self.gizmos.door
|
||||
for prop in door_gizmos.__annotations__:
|
||||
layout.prop(door_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")
|
||||
@@ -735,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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -100,7 +100,6 @@ class Blender(bonsai.core.tool.Blender):
|
||||
- (identifier, name, description, icon, number)
|
||||
"""
|
||||
BLENDER_ENUM_ITEMS = Iterable[BLENDER_ENUM_ITEM]
|
||||
BLENDER_5 = bpy.app.version >= (5, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def activate_camera(cls, obj: bpy.types.Object) -> None:
|
||||
@@ -417,7 +416,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {}
|
||||
) -> Union[bpy.types.ShaderNode, None]:
|
||||
"""returns first node from the `blender_material` shader graph with type `node_type`"""
|
||||
if not tool.Style.get_use_nodes(blender_material):
|
||||
if not blender_material.use_nodes:
|
||||
return
|
||||
nodes = blender_material.node_tree.nodes
|
||||
for node in nodes:
|
||||
@@ -710,7 +709,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
new_selected_objects = [o for o in selected_objects if cls.is_valid_data_block(o) and o in view_layer_objects]
|
||||
|
||||
if active_object and not cls.is_valid_data_block(active_object):
|
||||
if active_object and (not cls.is_valid_data_block(active_object) or active_object not in view_layer_objects):
|
||||
active_object = None
|
||||
|
||||
return context, active_object, new_selected_objects
|
||||
@@ -1065,7 +1064,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
"""Tries to validate the current BIM modifier parameters for the active object
|
||||
Goes into path editing mode if the modifier supports it
|
||||
|
||||
:return: True if an action was taken, False otherwise
|
||||
Returns True if an action was taken, False otherwise
|
||||
"""
|
||||
if cls.is_roof(element):
|
||||
if cls.is_editing_roof_parameters(obj):
|
||||
@@ -1089,7 +1088,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
def try_canceling_editing_modifier_parameters_or_path(cls, obj: bpy.types.Object) -> bool:
|
||||
"""Tries to cancel the current BIM modifier parameters or path edition for the active object
|
||||
|
||||
:return: True if an action was taken, False otherwise
|
||||
Returns True if an action was taken, False otherwise
|
||||
"""
|
||||
if cls.is_editing_railing_path(obj):
|
||||
bpy.ops.bim.cancel_editing_railing_path()
|
||||
@@ -1672,10 +1671,12 @@ class Blender(bonsai.core.tool.Blender):
|
||||
- "user_interface.wcol_menu.text" (Menu Text)
|
||||
- "user_interface.wcol_menu.text_sel" (Menu Text Selected)
|
||||
|
||||
:param color_path: The attribute path relative to bpy.context.preferences.themes[0].
|
||||
:param threshold: The RGB sum threshold for determining dark mode. Default is 1.671.
|
||||
Args:
|
||||
color_path (str, optional): The attribute path relative to bpy.context.preferences.themes[0].
|
||||
threshold (float, optional): The RGB sum threshold for determining dark mode. Default is 1.671.
|
||||
|
||||
:return: 'dm' (dark mode) if the RGB sum is > threshold, otherwise 'lm' (light mode).
|
||||
Returns:
|
||||
str: 'dm' (dark mode) if the RGB sum is > threshold, otherwise 'lm' (light mode).
|
||||
"""
|
||||
full_path = f"bpy.context.preferences.themes[0].{color_path}"
|
||||
|
||||
@@ -2082,14 +2083,3 @@ class Blender(bonsai.core.tool.Blender):
|
||||
assert bpy.context.preferences
|
||||
if props_updated and bpy.context.preferences.use_preferences_save:
|
||||
bpy.ops.wm.save_userpref()
|
||||
|
||||
@classmethod
|
||||
def get_eevee_name(cls) -> Literal["BLENDER_EEVEE"] | Literal["BLENDER_EEVEE_NEXT"]:
|
||||
"""Convenience method to get correct eevee render engine name in multiple Blender versions.
|
||||
|
||||
In Blender 4.2 eevee was renamed to "eevee next".
|
||||
In Blender 5 eevee next is now just "eevee" again.
|
||||
"""
|
||||
if cls.BLENDER_5:
|
||||
return "BLENDER_EEVEE"
|
||||
return "BLENDER_EEVEE_NEXT"
|
||||
|
||||
+20
-208
@@ -33,7 +33,6 @@ import sys
|
||||
import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import numpy as np
|
||||
import mathutils.geometry
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
import itertools
|
||||
@@ -189,33 +188,6 @@ class Cad:
|
||||
"""
|
||||
return geometry.intersect_line_plane(v1, v2, plane_co, plane_no)
|
||||
|
||||
def intersect_edge_plane_v2(v1, v2, plane_co, plane_no, eps=1e-9):
|
||||
"""
|
||||
Numpy version of intersect_edge_plane
|
||||
> takes an edges as two vector, and a plane as origin point and normal
|
||||
< return the intersection point or None
|
||||
"""
|
||||
|
||||
# References: https://rosettacode.org/wiki/Find_the_intersection_of_a_line_with_a_plane#Python,
|
||||
# https://stackoverflow.com/a/18543221
|
||||
|
||||
p0 = np.array((v1.x, v1.y, v1.z), dtype=np.float64)
|
||||
p1 = np.array((v2.x, v2.y, v2.z), dtype=np.float64)
|
||||
pc = np.array((plane_co.x, plane_co.y, plane_co.z), dtype=np.float64)
|
||||
n = np.array((plane_no.x, plane_no.y, plane_no.z), dtype=np.float64)
|
||||
|
||||
u = p1 - p0
|
||||
dot = np.dot(u, n)
|
||||
if abs(dot) < eps:
|
||||
# Line is parallel to plane (no intersection or lies in plane)
|
||||
return None
|
||||
|
||||
w = pc - p0
|
||||
fac = np.dot(w, n) / dot
|
||||
p = p0 + fac * u
|
||||
|
||||
return Vector(p)
|
||||
|
||||
@classmethod
|
||||
def intersect_edges(
|
||||
cls, edge1: tuple[Vector, Vector], edge2: tuple[Vector, Vector]
|
||||
@@ -243,50 +215,47 @@ class Cad:
|
||||
def intersect_edges_v2(cls, edge1, edge2):
|
||||
"""
|
||||
Calculate the closest points on two line segments.
|
||||
Note: This function doesn't use intersect_line_line and uses Numpy for calculations
|
||||
Note: This function doesn't use intersect_line_line
|
||||
|
||||
> edge1: tuple of two vectors (v1, v2) representing the first segment
|
||||
> edge2: tuple of two vectors (v3, v4) representing the second segment
|
||||
< returns: tuple of two vectors (C1, C2) or (None, None) if lines are parallel
|
||||
"""
|
||||
# This function seems to work better than intersect_line_line
|
||||
# This function seems to work better then intersect_line_line
|
||||
# in orthogonal view
|
||||
# https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
|
||||
|
||||
is_2d = False
|
||||
# Starting and ending points
|
||||
p1, p1_end = edge1
|
||||
p2, p2_end = edge2
|
||||
if len(p1) == 2:
|
||||
P1, P1_end = edge1
|
||||
P2, P2_end = edge2
|
||||
if len(P1) == 2:
|
||||
is_2d = True
|
||||
p1, p1_end = p1.to_3d(), p1_end.to_3d()
|
||||
p2, p2_end = p2.to_3d(), p2_end.to_3d()
|
||||
P1, P1_end = P1.to_3d(), P1_end.to_3d()
|
||||
P2, P2_end = P2.to_3d(), P2_end.to_3d()
|
||||
|
||||
p1 = np.array((p1.x, p1.y, p1.z), dtype=np.float64)
|
||||
p1_end = np.array((p1_end.x, p1_end.y, p1_end.z), dtype=np.float64)
|
||||
p2 = np.array((p2.x, p2.y, p2.z), dtype=np.float64)
|
||||
p2_end = np.array((p2_end.x, p2_end.y, p2_end.z), dtype=np.float64)
|
||||
# Directions
|
||||
d1 = p1_end - p1
|
||||
d2 = p2_end - p2
|
||||
d1 /= np.linalg.norm(d1) or 1 # equivalent of Vector.normalized() or Vector / Vector.length
|
||||
d2 /= np.linalg.norm(d2) or 1
|
||||
d1 = (P1_end - P1).normalized()
|
||||
d2 = (P2_end - P2).normalized()
|
||||
|
||||
n = np.cross(d1, d2)
|
||||
n = d1.cross(d2)
|
||||
|
||||
# if n is zero, lines are parallel
|
||||
if abs(np.linalg.norm(n)) < 1e-6:
|
||||
if abs(n.length) < 1e-6:
|
||||
return None, None
|
||||
|
||||
n2 = np.cross(d2, n)
|
||||
c1 = p1 + (np.dot((p2 - p1), n2) / (np.dot(d1, n2))) * d1
|
||||
n1 = np.cross(d1, n)
|
||||
c2 = p2 + (np.dot((p1 - p2), n1) / (np.dot(d2, n1))) * d2
|
||||
n2 = d2.cross(n)
|
||||
|
||||
C1 = P1 + ((P2 - P1).dot(n2) / (d1.dot(n2))) * d1
|
||||
|
||||
n1 = d1.cross(n)
|
||||
|
||||
C2 = P2 + ((P1 - P2).dot(n1) / (d2.dot(n1))) * d2
|
||||
|
||||
if is_2d:
|
||||
return Vector(c1[:2].copy()), Vector(c2[:2].copy())
|
||||
return C1.to_2d(), C2.to_2d()
|
||||
else:
|
||||
return Vector(c1), Vector(c2)
|
||||
return C1, C2
|
||||
|
||||
@classmethod
|
||||
def get_intersection(cls, edge1, edge2):
|
||||
@@ -838,160 +807,3 @@ class Cad:
|
||||
v1 = v2
|
||||
|
||||
return new_verts
|
||||
|
||||
@classmethod
|
||||
def region_2d_to_vector_3d_np(cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector) -> Vector:
|
||||
"""
|
||||
Numpy version of view3d_utils.region_2d_to_vector_3d
|
||||
Return a direction vector from the viewport at the specific 2d region
|
||||
coordinate.
|
||||
|
||||
> region: region of the 3D viewport, typically bpy.context.region.
|
||||
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
|
||||
> coord: 2d coordinates relative to the region:
|
||||
(event.mouse_region_x, event.mouse_region_y) for example.
|
||||
< returns a normalized 3d vector.
|
||||
"""
|
||||
view_m = np.array(rv3d.view_matrix)
|
||||
window_m = np.array(rv3d.window_matrix)
|
||||
viewinv = np.linalg.inv(view_m)
|
||||
|
||||
if rv3d.is_perspective:
|
||||
# For better precision with large numbers, avoid using rv3d.perspective_matrix. See https://github.com/IfcOpenShell/IfcOpenShell/issues/7046
|
||||
# Calculate it with view_matrix and window_matrix as numpy arrays.
|
||||
pers_m = window_m @ view_m
|
||||
persinv = np.linalg.inv(pers_m)
|
||||
|
||||
out = np.array(
|
||||
[(2.0 * coord[0] / region.width) - 1.0, (2.0 * coord[1] / region.height) - 1.0, -0.5, 1.0],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
w = out[:3].dot(persinv[3, :3]) + persinv[3, 3]
|
||||
|
||||
viewinv_translation = viewinv[:3, 3]
|
||||
view_vector = (persinv.dot(out)[:3] / w) - viewinv_translation
|
||||
else:
|
||||
view_vector = -viewinv[:3, 2].copy() # -Z column
|
||||
|
||||
view_vector /= np.linalg.norm(view_vector) or 1 # equivalent to Vector.normalized()
|
||||
return Vector(view_vector)
|
||||
|
||||
@classmethod
|
||||
def region_2d_to_location_3d_np(
|
||||
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, depth_location: Vector
|
||||
) -> Vector:
|
||||
"""
|
||||
Numpy version of view3d_utils.region_2d_to_location_3d
|
||||
Return a 3d location from the region relative 2d coords, aligned with
|
||||
*depth_location*.
|
||||
|
||||
> region: region of the 3D viewport, typically bpy.context.region.
|
||||
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
|
||||
> coord: 2d coordinates relative to the region:
|
||||
(event.mouse_region_x, event.mouse_region_y) for example.
|
||||
< returns a normalized 3d vector.
|
||||
"""
|
||||
coord_vec = cls.region_2d_to_vector_3d_np(region, rv3d, coord)
|
||||
depth_location = np.array([depth_location[0], depth_location[1], depth_location[2]], dtype=np.float64)
|
||||
|
||||
origin_start = cls.region_2d_to_origin_3d_np(region, rv3d, coord)
|
||||
origin_end = origin_start + coord_vec
|
||||
|
||||
if rv3d.is_perspective:
|
||||
viewinv = np.linalg.inv(rv3d.view_matrix)
|
||||
view_vec = viewinv[:3, 2].copy()
|
||||
return cls.intersect_edge_plane_v2(
|
||||
Vector(origin_start), Vector(origin_end), Vector(depth_location), Vector(view_vec)
|
||||
)
|
||||
else:
|
||||
return cls.point_on_edge(
|
||||
Vector(depth_location),
|
||||
(Vector(origin_start), Vector(origin_end)),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def region_2d_to_origin_3d_np(
|
||||
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, *, clamp: float = None
|
||||
) -> Vector:
|
||||
"""
|
||||
Numpy version of view3d_utils.region_2d_to_origin_3d
|
||||
Return the 3d view origin from the region relative 2d coords.
|
||||
|
||||
.. note::
|
||||
|
||||
Orthographic views have a less obvious origin,
|
||||
the far clip is used to define the viewport near/far extents.
|
||||
Since far clip can be a very large value,
|
||||
the result may give with numeric precision issues.
|
||||
|
||||
To avoid this problem, you can optionally clamp the far clip to a
|
||||
smaller value based on the data you're operating on.
|
||||
|
||||
|
||||
> region: region of the 3D viewport, typically bpy.context.region.
|
||||
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
|
||||
> coord: 2d coordinates relative to the region:
|
||||
(event.mouse_region_x, event.mouse_region_y) for example.
|
||||
> clamp: clamp: Clamp the maximum far-clip value used.
|
||||
(negative value will move the offset away from the view_location)
|
||||
< returns the origin of the viewpoint in 3d space.
|
||||
"""
|
||||
|
||||
view_m = np.array(rv3d.view_matrix)
|
||||
window_m = np.array(rv3d.window_matrix)
|
||||
viewinv = np.linalg.inv(view_m)
|
||||
|
||||
if rv3d.is_perspective:
|
||||
origin_start = viewinv[:3, 3].copy()
|
||||
else:
|
||||
pers_m = window_m @ view_m # See https://github.com/IfcOpenShell/IfcOpenShell/issues/7046
|
||||
persinv = np.linalg.inv(pers_m)
|
||||
|
||||
dx = (2.0 * coord[0] / region.width) - 1.0
|
||||
dy = (2.0 * coord[1] / region.height) - 1.0
|
||||
|
||||
origin_start = (persinv[:3, 0] * dx) + (persinv[:3, 1] * dy) + persinv[:3, 3]
|
||||
|
||||
if clamp != 0.0:
|
||||
if rv3d.view_perspective != "CAMERA":
|
||||
origin_offset = persinv[:3, 2].copy() # column 2
|
||||
if clamp is not None:
|
||||
c = float(clamp)
|
||||
if c < 0.0:
|
||||
origin_offset = -origin_offset
|
||||
c = -c
|
||||
length = np.linalg.norm(origin_offset)
|
||||
if length > c and length > 0.0:
|
||||
origin_offset = (origin_offset / length) * c
|
||||
origin_start = origin_start - origin_offset
|
||||
|
||||
return Vector(origin_start)
|
||||
|
||||
@classmethod
|
||||
def location_3d_to_region_2d_np(
|
||||
cls, region: bpy.types.Region, rv3d: bpy.types.RegionView3d, coord: Vector, *, default=None
|
||||
) -> Vector:
|
||||
"""
|
||||
Numpy version of view3d_utils.location_3d_to_region_2d
|
||||
Return the *region* relative 2d location of a 3d position.
|
||||
|
||||
> region: region of the 3D viewport, typically bpy.context.region.
|
||||
> rv3d: 3D region data, typically bpy.context.space_data.region_3d.
|
||||
> coord: 2d coordinates relative to the region:
|
||||
(event.mouse_region_x, event.mouse_region_y) for example.
|
||||
< returns a 2d location.
|
||||
"""
|
||||
pt = np.array((coord[0], coord[1], coord[2], 1.0), dtype=np.float64)
|
||||
view_m = np.array(rv3d.view_matrix)
|
||||
window_m = np.array(rv3d.window_matrix)
|
||||
pers_m = window_m @ view_m
|
||||
prj = pers_m.dot(pt) # 4-vector
|
||||
w = prj[3]
|
||||
if w > 0.0:
|
||||
width_half = region.width / 2.0
|
||||
height_half = region.height / 2.0
|
||||
x = width_half + width_half * (prj[0] / w)
|
||||
y = height_half + height_half * (prj[1] / w)
|
||||
return Vector((float(x), float(y)))
|
||||
return default
|
||||
|
||||
@@ -143,11 +143,12 @@ class Debug(bonsai.core.tool.Debug):
|
||||
|
||||
Note that Styles UI (or other UI) should be updated manually after using this method.
|
||||
|
||||
:param object_type: The type of object to merge
|
||||
:param by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
|
||||
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
|
||||
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
|
||||
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
|
||||
Args:
|
||||
object_type: The type of object to merge
|
||||
by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
|
||||
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
|
||||
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
|
||||
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
|
||||
"""
|
||||
|
||||
def normalize_name(name: str) -> str:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user