mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c738c15d9 | |||
| 036ee098a6 |
@@ -53,7 +53,7 @@ jobs:
|
||||
python ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: mac-${{ matrix.arch }}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
python ../IfcOpenShell/nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
python3 ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
python3 ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: win-${{ matrix.arch }}
|
||||
# Windows ccache needs ~1GB
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: ci-lint
|
||||
name: ci-black-formatting
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,9 +7,6 @@ on:
|
||||
jobs:
|
||||
lint-formatting:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MIN_IOS_PY_VERSION: "3.10"
|
||||
MIN_BLENDER_PY_VERSION: "3.11"
|
||||
steps:
|
||||
- name: Action - checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -17,12 +14,12 @@ jobs:
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.MIN_IOS_PY_VERSION }}
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -30,7 +27,6 @@ jobs:
|
||||
uv tool install ruff
|
||||
uv tool install black
|
||||
uv tool install poethepoet
|
||||
uv tool install ty
|
||||
|
||||
# black doesn't catch all syntax errors, so we check them explicitly.
|
||||
- name: Check syntax errors
|
||||
@@ -39,8 +35,8 @@ jobs:
|
||||
ERROR=0
|
||||
# Using 2 Python versions - one minimum required for IfcOpenShell
|
||||
# and other that's used by Blender currently.
|
||||
python${{ env.MIN_IOS_PY_VERSION }} -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
||||
python${{ env.MIN_BLENDER_PY_VERSION }} -W error -m compileall -q src/bonsai || ERROR=1
|
||||
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
||||
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
|
||||
exit $ERROR
|
||||
continue-on-error: true
|
||||
|
||||
@@ -58,13 +54,6 @@ jobs:
|
||||
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
|
||||
continue-on-error: true
|
||||
|
||||
- name: ty check
|
||||
id: ty
|
||||
run: |
|
||||
poe ty-venv
|
||||
poe ty
|
||||
continue-on-error: true
|
||||
|
||||
- name: Ruff check
|
||||
id: ruff
|
||||
run: |
|
||||
@@ -113,7 +102,4 @@ jobs:
|
||||
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
|
||||
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
|
||||
fi
|
||||
if [ "${{ steps.ty.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check failed, see 'ty check' step for the details." && ERROR=1
|
||||
fi
|
||||
exit $ERROR
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
|
||||
|
||||
# Download Blender.
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.1/blender-5.1.0-linux-x64.tar.xz
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.0/blender-5.0.1-linux-x64.tar.xz
|
||||
tar -xf blender.tar.xz
|
||||
|
||||
# Setup Blender.
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
pip install -r requirements.txt
|
||||
python setup_extensions_repo.py --last-tag
|
||||
cd ..
|
||||
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py313*-linux-x64.zip)"
|
||||
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py311*-linux-x64.zip)"
|
||||
|
||||
# Install Bonsai.
|
||||
blender --command extension install-file -r user_default -e $bonsai_zip
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py311, py312, py313]
|
||||
pyver: [py311, py312]
|
||||
config:
|
||||
- {
|
||||
name: "Windows Build",
|
||||
@@ -42,11 +42,6 @@ jobs:
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1,
|
||||
}
|
||||
exclude:
|
||||
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
|
||||
- pyver: py313
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: ci-ifcedit-pypi
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifcedit &&
|
||||
make dist IS_STABLE=TRUE
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifcedit/dist
|
||||
@@ -1,36 +0,0 @@
|
||||
name: ci-ifcmcp-pypi
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifcmcp &&
|
||||
make dist IS_STABLE=TRUE
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifcmcp/dist
|
||||
verbose: true
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
environment-name: test-env
|
||||
create-args: >-
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
run: |
|
||||
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
|
||||
|
||||
- uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
environment-name: test-env
|
||||
create-args: >-
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
-
|
||||
name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
|
||||
-
|
||||
name: Build ifcopenshell
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
lfs: true
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v8.0.1
|
||||
uses: actions/download-artifact@v8.0.0
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py310, py311, py312, py313, py314]
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py310, py311, py312, py313, py314]
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: ci-ifcquery-pypi
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifcquery &&
|
||||
make dist IS_STABLE=TRUE
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifcquery/dist
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Release Pyodide WASM Wheel
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout IfcOpenShell
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Build wheel
|
||||
working-directory: pyodide
|
||||
run: uv run pack_wheel.py --build
|
||||
|
||||
- name: Find wheel
|
||||
id: wheel
|
||||
run: |
|
||||
WHEEL=$(ls pyodide/dist/ifcopenshell-*.whl)
|
||||
echo "path=$WHEEL" >> $GITHUB_OUTPUT
|
||||
echo "name=$(basename $WHEEL)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout wasm-wheels
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/wasm-wheels
|
||||
path: wasm-wheels
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
- name: Commit and push wheel to wasm-wheels
|
||||
run: |
|
||||
WHEEL_NAME="${{ steps.wheel.outputs.name }}"
|
||||
cp "${{ steps.wheel.outputs.path }}" "wasm-wheels/$WHEEL_NAME"
|
||||
cd wasm-wheels
|
||||
git config user.name "IfcOpenBot"
|
||||
git config user.email "ifcopenbot@ifcopenshell.org"
|
||||
git add "$WHEEL_NAME"
|
||||
git commit -m "Add $WHEEL_NAME"
|
||||
VERSION=$(cat ../VERSION)
|
||||
git tag "v${VERSION}"
|
||||
git push origin main
|
||||
git push origin "v${VERSION}"
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
libhdf5-dev libcgal-dev libeigen3-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
@@ -254,7 +254,6 @@ jobs:
|
||||
cd ../ifcpatch && make test || ERROR=1
|
||||
pip install -e ../ifctester --no-deps
|
||||
cd ../ifctester && make test || ERROR=1
|
||||
make build-ids-docs || ERROR=1
|
||||
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
|
||||
cd ../ifcopenshell-python
|
||||
pip install mathutils
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Build and Deploy Stable Documentation
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/bonsai/docs
|
||||
pip install -r requirements.txt # Run pip install from the docs directory
|
||||
|
||||
- name: Build documentation
|
||||
run: |
|
||||
cd src/bonsai/docs
|
||||
make html
|
||||
|
||||
- name: Deploy to GitHub Pages (Stable)
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
external_repository: IfcOpenShell/bonsaibim_org_docs
|
||||
publish_branch: main
|
||||
cname: docs.bonsaibim.org
|
||||
publish_dir: src/bonsai/docs/_build/html
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Deploy AI chat App to static page repo
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'src/ifcchat/**'
|
||||
- '.github/workflows/publish-aichat-app.yaml'
|
||||
branches:
|
||||
- v0.8.0
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (recursive)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Checkout intermediate Pages repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/aichat_ifcopenshell_org_static_html
|
||||
ref: gh-pages
|
||||
path: output
|
||||
token: ${{ secrets.WEBSITE_PUBLISH }}
|
||||
- name: Sync demo app into target subfolder
|
||||
run: |
|
||||
rsync -av --delete --exclude='.git/' src/ifcchat/ output/
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Download wheels
|
||||
working-directory: output/
|
||||
run: |
|
||||
pip download ifcquery==0.8.5 ifcopenshell-mcp==0.8.5 ifcedit==0.8.5 lark==1.3.1 isodate==0.7.2 --no-deps -d ./dist
|
||||
- name: Commit and push if changed
|
||||
working-directory: output
|
||||
run: |
|
||||
git config --global user.name 'IfcOpenBot'
|
||||
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
|
||||
|
||||
git add .
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "$(git log --oneline -1)"
|
||||
git push origin gh-pages
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Deploy Pyodide Demo App to static page repo
|
||||
name: Deploy Pyodide Demo App to GitHub Pages
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -11,7 +11,6 @@ on:
|
||||
- '.github/workflows/publish-pyodide-demo-app.yml'
|
||||
branches:
|
||||
- v0.8.0
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
@@ -31,27 +30,21 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Checkout intermediate Pages repo
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
repository: IfcOpenShell/wasm_ifcopenshell_org_static_html
|
||||
ref: gh-pages
|
||||
path: output
|
||||
token: ${{ secrets.WEBSITE_PUBLISH }}
|
||||
- name: Sync demo app into target subfolder
|
||||
run: |
|
||||
rsync -av --delete --exclude='.git/' src/pyodide/demo-app/ output/
|
||||
- name: Commit and push if changed
|
||||
working-directory: output
|
||||
run: |
|
||||
git config --global user.name 'IfcOpenBot'
|
||||
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
|
||||
path: src/pyodide/demo-app/
|
||||
|
||||
git add .
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "$(git log --oneline -1)"
|
||||
git push origin gh-pages
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
+3
-16
@@ -5,8 +5,6 @@
|
||||
/_installed-vs*-x*/
|
||||
/build/
|
||||
/src/examples/build/
|
||||
# ifctester docs output
|
||||
/src/ifctester/test/build/
|
||||
|
||||
# output directories
|
||||
/cmake/out/
|
||||
@@ -14,7 +12,6 @@
|
||||
/src/ifcmax/out/
|
||||
/src/ifcwrap/out/
|
||||
/src/qtviewer/out/
|
||||
/src/ifctester/webapp/public/pyodide/
|
||||
|
||||
/win/BuildDepsCache*.txt
|
||||
|
||||
@@ -83,14 +80,10 @@ src/ifcopenshell-python/test/build
|
||||
# bonsai i18n
|
||||
src/bonsai/bonsai/translations.py
|
||||
|
||||
# bonsai external dependencies (cloned for just ty checks)
|
||||
src/bonsai/external_dependencies/
|
||||
|
||||
# bonsai test temp/cache files
|
||||
# bonsai test temp files
|
||||
src/bonsai/test/files/temp
|
||||
src/bonsai/test/files/*.cache.blend
|
||||
src/bonsai/test/files/*.cache.json
|
||||
src/bonsai/test/files/*.cache.sqlite
|
||||
src/bonsai/test/files/basic.ifc.cache.blend
|
||||
src/bonsai/test/files/basic.ifc.cache.sqlite
|
||||
|
||||
# bonsai data
|
||||
src/bonsai/bonsai/bim/data/build/
|
||||
@@ -122,9 +115,3 @@ dev_environment.bat
|
||||
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||
|
||||
|
||||
# temp files from AI coding tools
|
||||
*.claude
|
||||
*.py.tmp*
|
||||
*.json.tmp*
|
||||
|
||||
@@ -50,14 +50,11 @@ Contents
|
||||
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
|
||||
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
|
||||
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||
| [ifcedit](https://docs.ifcopenshell.org/ifcedit.html) | CLI wrapper for ifcopenshell.api IFC model mutation functions | LGPL-3.0-or-later | [](https://pypi.org/project/ifcedit/) |
|
||||
| [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)
|
||||
| [ifcmcp](https://docs.ifcopenshell.org/ifcmcp.html) | MCP server for querying and editing IFC building models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcopenshell-mcp/) |
|
||||
| [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) [](https://github.com/IfcOpenShell/wasm-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) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
|
||||
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||
| [ifcquery](https://docs.ifcopenshell.org/ifcquery.html) | CLI tool for querying and inspecting IFC building models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcquery/) |
|
||||
| [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)
|
||||
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||
|
||||
The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
||||
|
||||
@@ -13,7 +13,6 @@ import hashlib
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
from typing import NoReturn
|
||||
from urllib import request
|
||||
|
||||
@@ -21,7 +20,7 @@ from github import Github
|
||||
|
||||
|
||||
def get_repo_tag_names() -> list[str]:
|
||||
git_return = subprocess.check_output("git tag -l", text=True)
|
||||
git_return = os.popen("git tag -l").read()
|
||||
tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name]
|
||||
print(f"{len(tag_names)} tag_names found in repo")
|
||||
return tag_names
|
||||
@@ -79,10 +78,6 @@ def get_release_zip(tag: str) -> tuple[str, str]:
|
||||
raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.")
|
||||
|
||||
|
||||
def run(command: str) -> None:
|
||||
subprocess.check_output(command)
|
||||
|
||||
|
||||
start = datetime.datetime.now()
|
||||
|
||||
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
|
||||
@@ -102,7 +97,7 @@ should_release = False
|
||||
target_release_tag = ""
|
||||
TARGET_OS = "windows-x64"
|
||||
|
||||
git_status = subprocess.check_output("git status", text=True)
|
||||
git_status = os.popen("git status").read()
|
||||
print(git_status)
|
||||
|
||||
for tag_name in get_repo_tag_names():
|
||||
@@ -152,7 +147,7 @@ blenderbim_build_version = target_release_tag.replace("blenderbim-", "")
|
||||
|
||||
# url_blenderbim_py3x_win_zip
|
||||
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
|
||||
subprocess.check_call(f"wget {url_blenderbim_py3x_win_zip} --no-verbose")
|
||||
os.popen(f"wget {url_blenderbim_py3x_win_zip} --no-verbose").read()
|
||||
|
||||
# sha256sum_blenderbim_py310_win_zip
|
||||
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
|
||||
@@ -206,13 +201,13 @@ print("[INFO] inserting dynamic chocolatey package parameters successful")
|
||||
print("\n_____ build choco.exe with mono")
|
||||
|
||||
choco_version = "1.1.0"
|
||||
run(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet")
|
||||
run(f"tar -xzf {choco_version}.tar.gz")
|
||||
os.popen(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet").read()
|
||||
os.popen(f"tar -xzf {choco_version}.tar.gz").read()
|
||||
print("choco tar unpack successful")
|
||||
os.chdir("choco-1.1.0")
|
||||
run("./build.sh")
|
||||
os.popen("./build.sh").read()
|
||||
|
||||
run("cp -r build_output/chocolatey /opt/chocolatey")
|
||||
os.popen("cp -r build_output/chocolatey /opt/chocolatey").read()
|
||||
os.chdir(BLENDERBIM_DIR)
|
||||
|
||||
if pathlib.Path("/opt/chocolatey/choco.exe").exists():
|
||||
@@ -220,15 +215,11 @@ if pathlib.Path("/opt/chocolatey/choco.exe").exists():
|
||||
|
||||
print("\n_____ build choco pack")
|
||||
|
||||
run("mono /opt/chocolatey/choco.exe pack --allow-unofficial")
|
||||
run(
|
||||
'mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial'
|
||||
)
|
||||
os.popen("mono /opt/chocolatey/choco.exe pack --allow-unofficial").read()
|
||||
os.popen('mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial').read()
|
||||
|
||||
print("\n_____ build choco push")
|
||||
run(
|
||||
'mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose'
|
||||
)
|
||||
os.popen('mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose').read()
|
||||
|
||||
print(f"choco push of version: {target_release_tag} successful!")
|
||||
print(f"it took: {datetime.datetime.now() - start}")
|
||||
|
||||
+3
-12
@@ -1094,19 +1094,10 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
|
||||
f"Python-{PYTHON_VERSION}.tgz",
|
||||
)
|
||||
python_install = INSTALL_DIR / f"python-{PYTHON_VERSION}"
|
||||
python_bin = python_install / "bin" / "python3"
|
||||
python_bin = INSTALL_DIR / f"python-{PYTHON_VERSION}" / "bin" / "python3"
|
||||
# `_ssl` module is present -> we will be able to install `numpy` later
|
||||
# to verify IfcOpenShell installation
|
||||
try:
|
||||
run([str(python_bin), "-c", "import _ssl"])
|
||||
except RuntimeError:
|
||||
print(
|
||||
"ERROR: Python was built without SSL support (_ssl module is missing). "
|
||||
f"To fix this: remove the installed Python at {python_install}; "
|
||||
"install OpenSSL development libraries and re-run."
|
||||
)
|
||||
raise
|
||||
run([str(python_bin), "-c", "import _ssl"])
|
||||
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
assert original_path
|
||||
@@ -1524,7 +1515,7 @@ if "IfcOpenShell-Python" in targets:
|
||||
)
|
||||
# Copy setup.py where pyodide build system expects it.
|
||||
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
|
||||
# Empty pyproject so it's contents won't affect the resulting wheel
|
||||
# Empty pyproject so it's contents won't affect the resulting wheelthe the
|
||||
# otherwise the wheel will use version and dependencies from toml, not setup.py.
|
||||
(REPO_PATH / "pyproject.toml").write_text("")
|
||||
|
||||
|
||||
@@ -41,9 +41,6 @@ def pack_dependencies(install_dir: Path) -> None:
|
||||
if not dependency_path.is_dir():
|
||||
continue
|
||||
dependency_name = dependency_path.name
|
||||
# Skip ifcopenshell - it's a build output, not a dependency to reuse across builds.
|
||||
if dependency_name == "ifcopenshell":
|
||||
continue
|
||||
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
|
||||
if tar_path.exists():
|
||||
print(f"Skipping existing cache: '{tar_path}'")
|
||||
|
||||
@@ -14,11 +14,18 @@ source .venv/bin/activate
|
||||
uv pip install pyodide-build
|
||||
# `uv run` is required, so xbuildenv would skip using `pip`.
|
||||
uv run pyodide xbuildenv install
|
||||
uv run pyodide xbuildenv install-emscripten
|
||||
|
||||
EMSDK_ROOT=$(pyodide config get emscripten_dir)
|
||||
source ${EMSDK_ROOT}/emsdk_env.sh
|
||||
# Emscripten doesn't come with xbuildenv.
|
||||
if [ ! -d emsdk ]; then
|
||||
git clone https://github.com/emscripten-core/emsdk
|
||||
fi
|
||||
pushd emsdk
|
||||
PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)
|
||||
./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}
|
||||
./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}
|
||||
source emsdk_env.sh
|
||||
which emcc
|
||||
popd
|
||||
|
||||
mkdir -p packages/ifcopenshell
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
#
|
||||
# /// script
|
||||
# # Latest Pyodide build env versions are listed here:
|
||||
# # https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json
|
||||
# # https://github.com/pyodide/pyodide-build/blob/main/pyodide_build/xbuildenv_releases.py
|
||||
# requires-python = "==3.13.2"
|
||||
# dependencies = [
|
||||
# "requests",
|
||||
# "setuptools",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Pack an IfcOpenShell WASM wheel using Pyodide build system.
|
||||
|
||||
Usage:
|
||||
uv run make_wheel.py # Show this help
|
||||
uv run make_wheel.py --build # Build wheel
|
||||
uv run make_wheel.py --clean # Clean build artifacts and exit
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
|
||||
# Get repo root (parent of this script's parent directory)
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
PYODIDE_DIR = REPO_ROOT / "pyodide"
|
||||
BUILD_DIR = PYODIDE_DIR / "build"
|
||||
|
||||
# Hardcoded path (Windows packing workaround with --dev flag)
|
||||
PYODIDE_BUILD = Path(r"L:\Projects\Github\pyodide-build")
|
||||
|
||||
# Wheel platform tag (from PYODIDE_EMSCRIPTEN_VERSION in pyodide-build/Makefile.envs)
|
||||
WHEEL_PLATFORM_TAG = "emscripten_4_0_9_wasm32"
|
||||
|
||||
# Location where ifcopenshell will be extracted
|
||||
IFCOPENSHELL_DIR = PYODIDE_DIR / "ifcopenshell"
|
||||
|
||||
|
||||
class WheelBuilder:
|
||||
@staticmethod
|
||||
def extract_ifcopenshell_from_git(dst: Path) -> None:
|
||||
"""Extract ifcopenshell directory from git repo into destination."""
|
||||
Tools.rmrf(dst)
|
||||
|
||||
print(f"Extracting ifcopenshell from git to {dst}...")
|
||||
# Use git ls-files piped to git checkout-index to avoid copying
|
||||
# untracked or ignored files from the actual repo.
|
||||
ls_proc = subprocess.Popen(
|
||||
["git", "ls-files", "-z", "src/ifcopenshell-python/ifcopenshell"],
|
||||
cwd=REPO_ROOT,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
checkout_proc = subprocess.Popen(
|
||||
["git", "checkout-index", "-z", "--prefix", "pyodide/", "--stdin"],
|
||||
cwd=REPO_ROOT,
|
||||
stdin=ls_proc.stdout,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
assert ls_proc.stdout is not None
|
||||
ls_proc.stdout.close()
|
||||
checkout_proc.communicate()
|
||||
|
||||
if checkout_proc.returncode != 0:
|
||||
assert checkout_proc.stderr is not None
|
||||
raise RuntimeError(f"Failed to extract: {checkout_proc.stderr.decode()}")
|
||||
|
||||
# Move src/ifcopenshell-python/ifcopenshell to ifcopenshell.
|
||||
temp_src = PYODIDE_DIR / "src" / "ifcopenshell-python" / "ifcopenshell"
|
||||
shutil.move(temp_src, dst)
|
||||
|
||||
# Clean up temporary src directory.
|
||||
Tools.rmrf(PYODIDE_DIR / "src")
|
||||
|
||||
print("✓ Extracted ifcopenshell from git")
|
||||
|
||||
@staticmethod
|
||||
def get_wheel_url(makefile_path: Path) -> str:
|
||||
"""Get S3 wheel URL based on BINARY_VERSION and BUILD_COMMIT from Makefile."""
|
||||
|
||||
def parse_makefile_vars() -> dict[str, str]:
|
||||
content = makefile_path.read_text()
|
||||
vars: dict[str, str] = {}
|
||||
for match in re.finditer(r"^(BINARY_VERSION|BUILD_COMMIT):=(.+)$", content, re.MULTILINE):
|
||||
vars[match.group(1)] = match.group(2).strip()
|
||||
return vars
|
||||
|
||||
vars: dict[str, str] = parse_makefile_vars()
|
||||
binary_version = vars["BINARY_VERSION"]
|
||||
build_commit = vars["BUILD_COMMIT"]
|
||||
filename = f"ifcopenshell-{binary_version}+{build_commit}-cp313-cp313-pyodide_2025_0_wasm32.whl"
|
||||
encoded_filename = quote(filename, safe="")
|
||||
return f"https://s3.amazonaws.com/ifcopenshell-builds/{encoded_filename}"
|
||||
|
||||
@staticmethod
|
||||
def download_and_extract_so(url: str, build_dir: Path) -> tuple[Path, Path]:
|
||||
"""Download wheel from URL and extract .so and .py files."""
|
||||
py_wrapper_filename = "ifcopenshell_wrapper.py"
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
wheel_path = build_dir / url.rsplit("/", 1)[-1]
|
||||
|
||||
if wheel_path.exists():
|
||||
print(f"Using cached wheel: {wheel_path}")
|
||||
else:
|
||||
print(f"Downloading {url}...")
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
wheel_path.write_bytes(response.content)
|
||||
|
||||
print("Extracting _ifcopenshell_wrapper files...")
|
||||
with zipfile.ZipFile(wheel_path) as zf:
|
||||
so_files = [f for f in zf.namelist() if f.endswith(".so")]
|
||||
py_files = [f for f in zf.namelist() if f.endswith(py_wrapper_filename)]
|
||||
|
||||
assert so_files, "No .so file found in wheel"
|
||||
assert py_files, f"No {py_wrapper_filename} file found in wheel"
|
||||
|
||||
so_file = so_files[0]
|
||||
so_dst = build_dir / Path(so_file).name
|
||||
so_dst.write_bytes(zf.read(so_file))
|
||||
|
||||
py_file = py_files[0]
|
||||
py_dst = build_dir / Path(py_file).name
|
||||
py_dst.write_bytes(zf.read(py_file))
|
||||
|
||||
return so_dst, py_dst
|
||||
|
||||
|
||||
class Tools:
|
||||
@staticmethod
|
||||
def run(
|
||||
cmd: list[str],
|
||||
cwd: Path | None = None,
|
||||
) -> None:
|
||||
print(f"$ {' '.join(cmd)}")
|
||||
subprocess.check_call(cmd, cwd=cwd)
|
||||
|
||||
@staticmethod
|
||||
def create_symlink(dst: Path, src: Path) -> None:
|
||||
Tools.rmrf(dst)
|
||||
dst.symlink_to(src)
|
||||
|
||||
@staticmethod
|
||||
def rmrf(path: Path) -> None:
|
||||
if path.exists() or path.is_symlink():
|
||||
if path.is_dir() and not path.is_symlink():
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
path.unlink()
|
||||
|
||||
|
||||
def clean() -> None:
|
||||
"""Remove build artifacts."""
|
||||
paths_to_remove = (
|
||||
BUILD_DIR,
|
||||
PYODIDE_DIR / ".pyodide_build",
|
||||
PYODIDE_DIR / "dist",
|
||||
PYODIDE_DIR / "ifcopenshell.egg-info",
|
||||
PYODIDE_DIR / "src",
|
||||
IFCOPENSHELL_DIR,
|
||||
)
|
||||
for path in paths_to_remove:
|
||||
if path.exists() or path.is_symlink():
|
||||
print(f"Removing {path}...")
|
||||
Tools.rmrf(path)
|
||||
print("✓ Clean complete")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__, add_help=False)
|
||||
parser.add_argument("--build", action="store_true", help="Build the wheel")
|
||||
parser.add_argument("--clean", action="store_true", help="Clean build folder")
|
||||
parser.add_argument(
|
||||
"--dev",
|
||||
action="store_true",
|
||||
help="Use editable pyodide-build from hardcoded path (Windows packing workaround)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.build and not args.clean:
|
||||
print(__doc__)
|
||||
return
|
||||
|
||||
if args.clean:
|
||||
clean()
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
WheelBuilder.extract_ifcopenshell_from_git(IFCOPENSHELL_DIR)
|
||||
|
||||
print("Downloading and extracting _ifcopenshell_wrapper files...")
|
||||
makefile = REPO_ROOT / "src" / "ifcopenshell-python" / "Makefile"
|
||||
wheel_url = WheelBuilder.get_wheel_url(makefile)
|
||||
so_file, py_file = WheelBuilder.download_and_extract_so(wheel_url, BUILD_DIR)
|
||||
|
||||
Tools.create_symlink(IFCOPENSHELL_DIR / Path(so_file).name, so_file)
|
||||
Tools.create_symlink(IFCOPENSHELL_DIR / Path(py_file).name, py_file)
|
||||
|
||||
print("Installing pyodide-build...")
|
||||
if args.dev:
|
||||
Tools.run(["uv", "pip", "install", "-e", str(PYODIDE_BUILD)])
|
||||
else:
|
||||
Tools.run(["uv", "pip", "install", "pyodide-build"])
|
||||
|
||||
print("Building with pyodide...")
|
||||
# Use --no-isolation due to pyodide-build Windows support issues:
|
||||
# symlink_unisolated_packages fails with missing `_sysconfigdata_$(CPYTHON_ABI_FLAGS)_emscripten_wasm32-emscripten.py`.
|
||||
# Hardcode platform name since pyodide doesn't yet support overriding wheel tags on Windows.
|
||||
#
|
||||
# Use `LEGACY_PLATFORM` since pyodide 0.34.1 introduced new tag for wheels `pyemscripten`,
|
||||
# which doesn't work with pyodide itself yet - https://github.com/pyodide/pyodide/issues/6177.
|
||||
os.environ["USE_LEGACY_PLATFORM"] = "1"
|
||||
Tools.run(["pyodide", "build", f"-C--build-option=--plat-name={WHEEL_PLATFORM_TAG}"])
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"\n✓ Done! ({elapsed:.1f}s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-39
@@ -2,16 +2,12 @@
|
||||
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
|
||||
# and we need it to get the wheel suffix right.
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
from setuptools import Extension, find_packages, setup
|
||||
from setuptools.command.build_ext import build_ext
|
||||
|
||||
# Detect repo folder: if setup.py is in pyodide folder, go to parent
|
||||
SETUP_DIR = Path(__file__).parent
|
||||
REPO_FOLDER = SETUP_DIR.parent if SETUP_DIR.name == "pyodide" else SETUP_DIR
|
||||
REPO_FOLDER = Path(__file__).parent
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
@@ -29,39 +25,6 @@ def get_dependencies() -> list[str]:
|
||||
return dependencies
|
||||
|
||||
|
||||
class UnixBuildExt(build_ext):
|
||||
"""Customize ``build_ext`` to support packing on Windows."""
|
||||
|
||||
def finalize_options(self):
|
||||
from distutils import sysconfig
|
||||
|
||||
super().finalize_options()
|
||||
if sys.platform == "win32":
|
||||
self.compiler = "unix"
|
||||
|
||||
# Configure sysconfig for Windows builds
|
||||
# CCSHARED is the only variable that's not customizable with env vars.
|
||||
# Basically avoiding this:
|
||||
# File ".venv\Lib\site-packages\setuptools\_distutils\sysconfig.py", line 366, in customize_compiler
|
||||
# compiler_so=cc_cmd + ' ' + ccshared,
|
||||
# ~~~~~~~~~~~~~^~~~~~~~~~
|
||||
# TypeError: can only concatenate str (not "NoneType") to str
|
||||
sysconfig.get_config_vars() # Initialize config cache
|
||||
if sysconfig._config_vars.get("CCSHARED") is None:
|
||||
sysconfig._config_vars["CCSHARED"] = "-fPIC"
|
||||
# Override compiler type before it's instantiated
|
||||
|
||||
# Set Emscripten compiler environment variables
|
||||
os.environ["CC"] = "emcc"
|
||||
os.environ["CXX"] = "em++"
|
||||
os.environ["CFLAGS"] = ""
|
||||
os.environ["CXXFLAGS"] = ""
|
||||
os.environ["LDSHARED"] = "emcc -shared"
|
||||
os.environ["AR"] = "emar"
|
||||
os.environ["ARFLAGS"] = "rcs"
|
||||
os.environ["SETUPTOOLS_EXT_SUFFIX"] = ".cpython-313-wasm32-emscripten.so"
|
||||
|
||||
|
||||
setup(
|
||||
name="ifcopenshell",
|
||||
version=get_version(),
|
||||
@@ -81,5 +44,4 @@ setup(
|
||||
},
|
||||
# Has to provide extension to get the correct wheel suffix.
|
||||
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
|
||||
cmdclass={"build_ext": UnixBuildExt},
|
||||
)
|
||||
|
||||
+2
-178
@@ -3,10 +3,9 @@ name = "IfcOpenShell"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"black==26.3.1",
|
||||
"ruff==0.15.9",
|
||||
"ruff==0.15.5",
|
||||
"poethepoet",
|
||||
"ty==0.0.29",
|
||||
"gersemi==0.26.1",
|
||||
"gersemi==0.26.0",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
@@ -29,9 +28,6 @@ extend-exclude = '''
|
||||
reportInvalidTypeForm = false
|
||||
disableBytesTypePromotions = true
|
||||
reportUnnecessaryTypeIgnoreComment = true
|
||||
reportRedeclaration = false
|
||||
# Ignore warnings from bpy stubs missing actual source files.
|
||||
reportMissingModuleSource = false
|
||||
# Pylance doesn't respect gitignore, so we have to exclude files manually here
|
||||
# to avoid VS Code slowing down.
|
||||
# https://github.com/microsoft/pylance-release/issues/5169
|
||||
@@ -82,137 +78,6 @@ ignore = [
|
||||
"UP032", # Replace .format with f-string
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
all = "ignore"
|
||||
|
||||
# Structural rules (no deep type inference needed, easier to adapt).
|
||||
abstract-method-in-final-class = "error"
|
||||
ambiguous-protocol-member = "error"
|
||||
conflicting-declarations = "error"
|
||||
conflicting-metaclass = "error"
|
||||
cyclic-class-definition = "error"
|
||||
cyclic-type-alias-definition = "error"
|
||||
dataclass-field-order = "error"
|
||||
duplicate-base = "error"
|
||||
duplicate-kw-only = "error"
|
||||
empty-body = "error"
|
||||
escape-character-in-forward-annotation = "error"
|
||||
final-on-non-method = "error"
|
||||
final-without-value = "error"
|
||||
ignore-comment-unknown-rule = "error"
|
||||
implicit-concatenated-string-type-annotation = "error"
|
||||
inconsistent-mro = "error"
|
||||
ineffective-final = "error"
|
||||
instance-layout-conflict = "error"
|
||||
invalid-dataclass = "error"
|
||||
invalid-dataclass-override = "error"
|
||||
invalid-enum-member-annotation = "error"
|
||||
invalid-explicit-override = "error"
|
||||
invalid-frozen-dataclass-subclass = "error"
|
||||
invalid-generic-class = "error"
|
||||
invalid-generic-enum = "error"
|
||||
invalid-ignore-comment = "error"
|
||||
invalid-legacy-positional-parameter = "error"
|
||||
invalid-legacy-type-variable = "error"
|
||||
invalid-named-tuple = "error"
|
||||
invalid-newtype = "error"
|
||||
invalid-overload = "error"
|
||||
invalid-paramspec = "error"
|
||||
invalid-protocol = "error"
|
||||
invalid-syntax-in-forward-annotation = "error"
|
||||
invalid-total-ordering = "error"
|
||||
invalid-type-alias-type = "error"
|
||||
invalid-type-checking-constant = "error"
|
||||
invalid-type-guard-definition = "error"
|
||||
invalid-type-variable-bound = "error"
|
||||
invalid-type-variable-constraints = "error"
|
||||
invalid-typed-dict-header = "error"
|
||||
invalid-typed-dict-statement = "error"
|
||||
override-of-final-method = "error"
|
||||
override-of-final-variable = "error"
|
||||
possibly-missing-import = "error"
|
||||
possibly-missing-submodule = "error"
|
||||
# Has false positives due to ty walrus operator bug.
|
||||
# possibly-unresolved-reference = "error"
|
||||
raw-string-type-annotation = "error"
|
||||
redundant-final-classvar = "error"
|
||||
shadowed-type-variable = "error"
|
||||
subclass-of-final-class = "error"
|
||||
super-call-in-named-tuple-method = "error"
|
||||
unavailable-implicit-super-arguments = "error"
|
||||
unbound-type-variable = "error"
|
||||
undefined-reveal = "error"
|
||||
unresolved-global = "error"
|
||||
unresolved-import = "error"
|
||||
unresolved-reference = "error"
|
||||
unused-ignore-comment = "error"
|
||||
unused-type-ignore-comment = "error"
|
||||
useless-overload-body = "error"
|
||||
|
||||
# Non-structural rules:
|
||||
deprecated = "error"
|
||||
zero-stepsize-in-slice = "error"
|
||||
possibly-missing-implicit-call = "error"
|
||||
unused-awaitable = "error"
|
||||
|
||||
# Function argument rules:
|
||||
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
|
||||
# call-non-callable = "error"
|
||||
conflicting-argument-forms = "error"
|
||||
# Too many false positives.
|
||||
# invalid-argument-type = "error"
|
||||
missing-argument = "error"
|
||||
parameter-already-assigned = "error"
|
||||
positional-only-parameter-as-kwarg = "error"
|
||||
too-many-positional-arguments = "error"
|
||||
unknown-argument = "error"
|
||||
# Has a lot of warnings due to current ty walrus operator issues.
|
||||
# index-out-of-bounds = "error"
|
||||
# unresolved-attribute = "error"
|
||||
|
||||
[tool.ty.environment]
|
||||
extra-paths = [
|
||||
"src/bonsai/external_dependencies",
|
||||
"src/bcf",
|
||||
"src/bsdd",
|
||||
"src/bonsai",
|
||||
"src/ifc4d",
|
||||
"src/ifc5d",
|
||||
"src/ifccityjson",
|
||||
"src/ifcclash",
|
||||
"src/ifccsv",
|
||||
"src/ifcdiff",
|
||||
"src/ifcfm",
|
||||
"src/ifcopenshell-python",
|
||||
"src/ifcpatch",
|
||||
"src/ifctester",
|
||||
]
|
||||
|
||||
[tool.ty.src]
|
||||
exclude = [
|
||||
# External dependencies cloned for type checking only.
|
||||
"src/bonsai/external_dependencies",
|
||||
# Submodules.
|
||||
"src/ifcopenshell-python/ifcopenshell/express",
|
||||
"src/ifcopenshell-python/ifcopenshell/mvd",
|
||||
"src/ifcopenshell-python/ifcopenshell/simple_spf",
|
||||
"src/svgfill/3rdparty",
|
||||
# Has special dependencies.
|
||||
"src/ifcopenshell-python/ifcopenshell/geom/app.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/geom/code_editor_pane.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/doc.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/ifc4x3dev_scrape_data_for_docs.py",
|
||||
# Too esoteric.
|
||||
"src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py",
|
||||
"src/ifc2ca/templates",
|
||||
# Too dev.
|
||||
"src/bcf/setup.py",
|
||||
"src/bsdd/yml_to_classes.py",
|
||||
# Deprecated.
|
||||
"src/ifc2ca/_deprecated",
|
||||
]
|
||||
|
||||
[tool.poe.tasks]
|
||||
|
||||
ruff-main = "ruff check --extend-exclude nix/build-all.py"
|
||||
@@ -222,47 +87,6 @@ ruff.sequence = ["ruff-main", "ruff-old"]
|
||||
|
||||
black = "black ."
|
||||
|
||||
ty.sequence = ["ty-bonsai", "ty-ios"]
|
||||
ty.help = "Run ty type checker. Requires ty-venv to be set up first."
|
||||
ty-bonsai = "ty check src/bonsai --python=src/bonsai/.venv"
|
||||
|
||||
ty-venv.sequence = ["bonsai-deps", "ty-venv-bonsai", "ty-venv-ios"]
|
||||
|
||||
ty-venv-bonsai.sequence = [
|
||||
{cmd = "uv venv src/bonsai/.venv --python=3.11 --allow-existing"},
|
||||
{cmd = "uv pip install -r src/bonsai/type-check-requirements.txt --python=src/bonsai/.venv"},
|
||||
]
|
||||
|
||||
ty-venv-ios.sequence = [
|
||||
{cmd = "uv venv src/ifcopenshell-python/.venv --python=3.10 --allow-existing"},
|
||||
{cmd = "uv pip install -r src/ifcopenshell-python/type-check-requirements.txt --python=src/ifcopenshell-python/.venv"},
|
||||
]
|
||||
|
||||
format.sequence = ["black", "ruff-main", "ruff-old"]
|
||||
|
||||
cmake-format = "gersemi . --in-place"
|
||||
|
||||
[tool.poe.tasks.ty-ios]
|
||||
# --ignore unresolved-reference: walrus operator false positives in ty.
|
||||
cmd = """
|
||||
ty check
|
||||
src/bcf
|
||||
src/bsdd
|
||||
src/ifc2ca
|
||||
src/ifc4d
|
||||
src/ifc5d
|
||||
src/ifccityjson
|
||||
src/ifcclash
|
||||
src/ifccsv
|
||||
src/ifcdiff
|
||||
src/ifcfm
|
||||
src/ifcopenshell-python
|
||||
src/ifcpatch
|
||||
src/ifctester
|
||||
--python=src/ifcopenshell-python/.venv
|
||||
--ignore unresolved-reference
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.bonsai-deps]
|
||||
help = "Clone or update Bonsai external dependencies."
|
||||
cmd = "python src/bonsai/scripts/bonsai_deps.py"
|
||||
|
||||
@@ -34,8 +34,8 @@ client_id, client_secret = "", ""
|
||||
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||
self.server.auth_code = query.get("code", [""])[0]
|
||||
self.server.auth_state = query.get("state", [""])[0]
|
||||
self.server.auth_code = query.get("code", [""])[0] # type: ignore
|
||||
self.server.auth_state = query.get("state", [""])[0] # type: ignore
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/plain")
|
||||
self.end_headers()
|
||||
@@ -255,7 +255,7 @@ class BcfClient:
|
||||
project_id: str = "",
|
||||
topics: str = "",
|
||||
query_string: Optional[str] = None,
|
||||
) -> None:
|
||||
) -> list[Any]:
|
||||
# return self.get(
|
||||
# f"/projects/{project_id}/topics",
|
||||
# {
|
||||
|
||||
@@ -173,17 +173,16 @@ def assert_viewpoints(viewpoints):
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
|
||||
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=False,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=False,
|
||||
),
|
||||
exceptions=expected_exception,
|
||||
default_visibility=False,
|
||||
),
|
||||
@@ -194,7 +193,6 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
|
||||
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
|
||||
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
||||
field_of_view=60,
|
||||
aspect_ratio=1.0,
|
||||
),
|
||||
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
||||
)
|
||||
@@ -202,17 +200,16 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
|
||||
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=True,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=True,
|
||||
),
|
||||
exceptions=expected_exception,
|
||||
default_visibility=True,
|
||||
),
|
||||
@@ -223,7 +220,6 @@ def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, ex
|
||||
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
|
||||
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
||||
field_of_view=60,
|
||||
aspect_ratio=1.0,
|
||||
),
|
||||
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
||||
)
|
||||
|
||||
+5
-6
@@ -48,7 +48,6 @@ VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
|
||||
VERSION_DATE:=$(shell date '+%y%m%d')
|
||||
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
|
||||
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
|
||||
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
|
||||
PYPI_IMP:=cp
|
||||
|
||||
ifdef PYVERSION
|
||||
@@ -64,7 +63,6 @@ PYNUMBER:=3$(PYMINOR)
|
||||
PYPI_VERSION:=3.$(PYMINOR)
|
||||
endif # def PYVERSION
|
||||
|
||||
IFCMERGE_VERSION:=2026-04-07
|
||||
|
||||
ifdef PLATFORM
|
||||
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
||||
@@ -234,16 +232,18 @@ endif
|
||||
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
|
||||
|
||||
# Required for hipped roof generation
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/prochitecture/bpypolyskel" --no-deps -w wheels/
|
||||
# TODO: Use official repo once https://github.com/prochitecture/bpypolyskel/pull/22 is merged.
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/bpypolyskel.git@pyproject_toml" --no-deps -w wheels/
|
||||
|
||||
# folder for executable files
|
||||
mkdir -p build/bonsai/libs/bin
|
||||
|
||||
# required for three-way git merging
|
||||
ifeq ($(PLATFORM), win)
|
||||
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/$(IFCMERGE_VERSION)/ifcmerge.exe
|
||||
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2025-01-26/ifcmerge.zip
|
||||
cd build/bonsai/libs/bin && unzip ifcmerge.zip && rm ifcmerge.zip
|
||||
else
|
||||
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/$(IFCMERGE_VERSION)/ifcmerge && chmod +x ifcmerge
|
||||
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge
|
||||
endif
|
||||
|
||||
# Generate translations module for Bonsai build
|
||||
@@ -262,7 +262,6 @@ else
|
||||
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
|
||||
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
|
||||
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
||||
endif
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
last_commit_hash = "8888888"
|
||||
last_commit_date = "9999999"
|
||||
last_git_branch = "7777777"
|
||||
|
||||
|
||||
def get_last_commit_hash() -> Union[str, None]:
|
||||
@@ -61,15 +60,6 @@ def get_last_commit_date() -> Union[str, None]:
|
||||
return last_commit_date
|
||||
|
||||
|
||||
def get_git_branch() -> Union[str, None]:
|
||||
# Using this weird way to write 7777777,
|
||||
# so makefile won't accidentally replace it here
|
||||
# we'll be able to distinguish branch from placeholder value.
|
||||
if last_git_branch == str(7_777777):
|
||||
return None
|
||||
return last_git_branch
|
||||
|
||||
|
||||
# Accessed from bonsai extension:
|
||||
bbim_semver: dict[str, Any] = {}
|
||||
|
||||
@@ -135,7 +125,6 @@ def get_debug_info(*, bonsai_failed_to_load: bool = False) -> dict[str, Any]:
|
||||
"bonsai_version": bbim_version,
|
||||
"bonsai_commit_hash": get_last_commit_hash(),
|
||||
"bonsai_commit_date": get_last_commit_date(),
|
||||
"bonsai_git_branch": get_git_branch(),
|
||||
"last_actions": last_actions,
|
||||
"last_error": last_error,
|
||||
}
|
||||
@@ -262,12 +251,10 @@ if IN_BLENDER:
|
||||
|
||||
global last_commit_hash
|
||||
global last_commit_date
|
||||
global last_git_branch
|
||||
path = Path(__file__).resolve().parent
|
||||
repo = git.Repo(str(path), search_parent_directories=True)
|
||||
last_commit_hash = repo.head.object.hexsha
|
||||
last_commit_date = repo.head.object.committed_datetime.isoformat()
|
||||
last_git_branch = repo.active_branch.name
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -72,7 +72,9 @@ class IfcExporter:
|
||||
|
||||
def set_header(self):
|
||||
self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
|
||||
self.file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
|
||||
self.file.header.file_name.time_stamp = (
|
||||
datetime.datetime.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
|
||||
)
|
||||
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
||||
self.file.header.file_name.originating_system = "{} {}".format(
|
||||
self.get_application_name(), tool.Blender.get_bonsai_version()
|
||||
|
||||
@@ -45,19 +45,16 @@ from bonsai.bim.module.nest.decorator import NestDecorator
|
||||
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
global_subscription_owner = object()
|
||||
# Separate owner for per-object msgbus subscriptions (name, active_material_index).
|
||||
# Using a dedicated owner allows clearing all per-object subscriptions at once
|
||||
# during undo/redo without affecting other global subscriptions.
|
||||
object_subscription_owner = object()
|
||||
|
||||
|
||||
def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None:
|
||||
try:
|
||||
obj.name
|
||||
except:
|
||||
# The object is invalid but somehow still has a callback.
|
||||
# This can occur during undo/redo when the Python wrapper is stale.
|
||||
return
|
||||
# The object is invalid but somehow still has a callback. Clear all
|
||||
# msgbus subscriptions to prevent useless further triggers.
|
||||
bpy.msgbus.clear_by_owner(obj)
|
||||
return # In case the object RNA is gone during an undo / redo operation
|
||||
# Blender names are up to 63 UTF-8 bytes
|
||||
if len(bytes(obj.name, "utf-8")) >= 63:
|
||||
return
|
||||
@@ -192,7 +189,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
|
||||
return
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=subscribe_to,
|
||||
owner=object_subscription_owner,
|
||||
owner=obj,
|
||||
args=(
|
||||
obj,
|
||||
data_path,
|
||||
|
||||
@@ -316,8 +316,11 @@ class IfcStore:
|
||||
del IfcStore.id_map[data["id"]]
|
||||
if "guid" in data:
|
||||
del IfcStore.guid_map[data["guid"]]
|
||||
# Note: msgbus subscriptions are cleared globally during
|
||||
# rebuild_element_maps which runs after every undo/redo.
|
||||
obj = IfcStore.get_object_by_name(data["obj"])
|
||||
if obj is None:
|
||||
# obj was just created during this step and didn't existed before.
|
||||
return
|
||||
bpy.msgbus.clear_by_owner(obj)
|
||||
|
||||
@staticmethod
|
||||
def commit_link_element(data: OperationData) -> None:
|
||||
@@ -364,8 +367,10 @@ class IfcStore:
|
||||
del IfcStore.id_map[data["id"]]
|
||||
if "guid" in data:
|
||||
del IfcStore.guid_map[data["guid"]]
|
||||
# Note: msgbus subscriptions are cleared globally during
|
||||
# rebuild_element_maps which runs after every undo/redo.
|
||||
obj = IfcStore.get_object_by_name(data["obj"])
|
||||
# obj might be removed after unlink.
|
||||
if not obj:
|
||||
bpy.msgbus.clear_by_owner(obj)
|
||||
|
||||
@staticmethod
|
||||
def unlink_element(
|
||||
|
||||
@@ -64,8 +64,8 @@ class MaterialCreator:
|
||||
mesh: Union[OBJECT_DATA_TYPE, None],
|
||||
shape_has_openings: bool,
|
||||
) -> None:
|
||||
if ((rep := getattr(element, "Representation", ...)) is not ... and not rep) or (
|
||||
(rep := getattr(element, "RepresentationMaps", ...)) is not ... and not rep
|
||||
if ((rep := getattr(element, "Representation", ...) is not ...) and not rep) or (
|
||||
(rep := getattr(element, "RepresentationMaps", ...) is not ...) and not rep
|
||||
):
|
||||
return
|
||||
|
||||
|
||||
@@ -73,22 +73,6 @@ def poll_related_object(self: "BIMObjectAggregateProperties", related_obj: bpy.t
|
||||
return True
|
||||
|
||||
|
||||
def update_relating_object(self, context):
|
||||
if self.relating_object:
|
||||
ifc_id = tool.Blender.get_object_bim_props(self.relating_object).ifc_definition_id
|
||||
if ifc_id:
|
||||
bpy.ops.bim.aggregate_assign_object(relating_object=ifc_id)
|
||||
bpy.ops.bim.disable_editing_aggregate()
|
||||
|
||||
|
||||
def update_related_object(self, context):
|
||||
if self.related_object:
|
||||
ifc_id = tool.Blender.get_object_bim_props(self.related_object).ifc_definition_id
|
||||
if ifc_id:
|
||||
bpy.ops.bim.aggregate_assign_object(related_object=ifc_id)
|
||||
bpy.ops.bim.disable_editing_aggregate()
|
||||
|
||||
|
||||
def update_aggregate_decorator(self, context):
|
||||
if self.aggregate_decorator:
|
||||
AggregateDecorator.install(bpy.context)
|
||||
@@ -105,15 +89,12 @@ def update_aggregate_mode_decorator(self, context):
|
||||
|
||||
class BIMObjectAggregateProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_object: PointerProperty(
|
||||
name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object, update=update_relating_object
|
||||
)
|
||||
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object)
|
||||
related_object: PointerProperty(
|
||||
name="Related Part",
|
||||
description="Related Part, will be used to derive the Relating Object",
|
||||
type=bpy.types.Object,
|
||||
poll=poll_related_object,
|
||||
update=update_related_object,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -295,13 +295,13 @@ class ExplorerShowUIPopup(bpy.types.Operator):
|
||||
bl_description = "Show Explorer UI to select element as attribute value or edit it."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
ifc_class: bpy.props.StringProperty()
|
||||
ifc_class: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""Element IFC class."""
|
||||
attribute_name: bpy.props.StringProperty()
|
||||
attribute_name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""IFC class attribute name."""
|
||||
data_path: bpy.props.StringProperty()
|
||||
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""Full data path"""
|
||||
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
||||
"""IFC id to preselect in the popup."""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -41,7 +41,7 @@ class BIMAttributeProperties(PropertyGroup):
|
||||
|
||||
|
||||
class ExplorerEntity(PropertyGroup):
|
||||
ifc_definition_id: bpy.props.IntProperty()
|
||||
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ifc_definition_id: int
|
||||
@@ -60,7 +60,7 @@ class BIMExplorerProperties(PropertyGroup):
|
||||
self.property_unset("editing_entity_id")
|
||||
self.entity_attributes.clear()
|
||||
|
||||
is_loaded: BoolProperty(
|
||||
is_loaded: BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Toggle Explorer UI",
|
||||
update=update_is_loaded,
|
||||
)
|
||||
@@ -76,15 +76,15 @@ class BIMExplorerProperties(PropertyGroup):
|
||||
def update_ifc_class(self, context: object) -> None:
|
||||
tool.Attribute.refresh_uilist_entities()
|
||||
|
||||
ifc_class: EnumProperty(
|
||||
ifc_class: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="IFC Class To Search",
|
||||
items=get_ifc_class,
|
||||
update=update_ifc_class,
|
||||
)
|
||||
entities: CollectionProperty(type=ExplorerEntity)
|
||||
active_entity_index: IntProperty()
|
||||
editing_entity_id: IntProperty()
|
||||
entity_attributes: CollectionProperty(type=Attribute)
|
||||
entities: CollectionProperty(type=ExplorerEntity) # pyright: ignore[reportRedeclaration]
|
||||
active_entity_index: IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
editing_entity_id: IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
entity_attributes: CollectionProperty(type=Attribute) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_loaded: bool
|
||||
|
||||
@@ -230,7 +230,7 @@ class BcfTopic(PropertyGroup):
|
||||
|
||||
|
||||
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
global RELATED_TOPICS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
global RELATED_TOPICS_ENUM_ITEMS
|
||||
props = self
|
||||
active_topic = props.active_topic
|
||||
active_related_topics = active_topic.related_topics.keys()
|
||||
|
||||
@@ -377,8 +377,6 @@ class EnableEditingBoundary(bpy.types.Operator):
|
||||
obj = tool.Ifc.get_object(entity)
|
||||
if entity and obj:
|
||||
setattr(bprops, blender_property, obj)
|
||||
bprops.physical_or_virtual = boundary.PhysicalOrVirtualBoundary or "NOTDEFINED"
|
||||
bprops.internal_or_external = boundary.InternalOrExternalBoundary or "NOTDEFINED"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -394,8 +392,6 @@ class DisableEditingBoundary(bpy.types.Operator):
|
||||
bprops.is_editing = False
|
||||
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
|
||||
setattr(bprops, blender_property, None)
|
||||
bprops.physical_or_virtual = "NOTDEFINED"
|
||||
bprops.internal_or_external = "NOTDEFINED"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -415,8 +411,6 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = getattr(bprops, blender_property, None)
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
attributes[blender_property] = entity
|
||||
attributes["physical_or_virtual"] = bprops.physical_or_virtual
|
||||
attributes["internal_or_external"] = bprops.internal_or_external
|
||||
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
|
||||
bpy.ops.bim.disable_editing_boundary()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -21,7 +21,6 @@ from typing import TYPE_CHECKING, Union
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
PointerProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -51,43 +50,12 @@ def element_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object
|
||||
return False
|
||||
|
||||
|
||||
def get_internal_or_external_items(
|
||||
self: "BIMObjectBoundaryProperties", context: bpy.types.Context | None
|
||||
) -> list[tuple[str, str, str]]:
|
||||
items = [
|
||||
("INTERNAL", "Internal", ""),
|
||||
("EXTERNAL", "External", ""),
|
||||
]
|
||||
ifc = tool.Ifc.get()
|
||||
if not ifc or ifc.schema != "IFC2X3":
|
||||
items += [
|
||||
("EXTERNAL_EARTH", "External Earth", ""),
|
||||
("EXTERNAL_WATER", "External Water", ""),
|
||||
("EXTERNAL_FIRE", "External Fire", ""),
|
||||
]
|
||||
items.append(("NOTDEFINED", "Not Defined", ""))
|
||||
return items
|
||||
|
||||
|
||||
class BIMObjectBoundaryProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
|
||||
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
|
||||
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||
physical_or_virtual: EnumProperty(
|
||||
name="PhysicalOrVirtualBoundary",
|
||||
items=[
|
||||
("PHYSICAL", "Physical", ""),
|
||||
("VIRTUAL", "Virtual", ""),
|
||||
("NOTDEFINED", "Not Defined", ""),
|
||||
],
|
||||
default="NOTDEFINED",
|
||||
)
|
||||
internal_or_external: EnumProperty(
|
||||
name="InternalOrExternalBoundary",
|
||||
items=get_internal_or_external_items,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
@@ -95,8 +63,6 @@ class BIMObjectBoundaryProperties(PropertyGroup):
|
||||
related_building_element: Union[bpy.types.Object, None]
|
||||
parent_boundary: Union[bpy.types.Object, None]
|
||||
corresponding_boundary: Union[bpy.types.Object, None]
|
||||
physical_or_virtual: str
|
||||
internal_or_external: str # values depend on schema: IFC2X3 omits EXTERNAL_EARTH/WATER/FIRE
|
||||
|
||||
|
||||
class BIMBoundaryProperties(PropertyGroup):
|
||||
|
||||
@@ -77,10 +77,6 @@ class BIM_PT_Boundary(Panel):
|
||||
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
|
||||
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
|
||||
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
|
||||
row = self.layout.row()
|
||||
row.prop(self.bprops, "physical_or_virtual")
|
||||
row = self.layout.row()
|
||||
row.prop(self.bprops, "internal_or_external")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
|
||||
@@ -88,8 +84,6 @@ class BIM_PT_Boundary(Panel):
|
||||
self.draw_relation_data(boundary, "RelatedBuildingElement")
|
||||
self.draw_relation_data(boundary, "ParentBoundary")
|
||||
self.draw_relation_data(boundary, "CorrespondingBoundary")
|
||||
self.draw_enum_data(boundary, "PhysicalOrVirtualBoundary")
|
||||
self.draw_enum_data(boundary, "InternalOrExternalBoundary")
|
||||
if hasattr(boundary, "InnerBoundaries"):
|
||||
for i, inner_boundary in enumerate(getattr(boundary, "InnerBoundaries", ())):
|
||||
row = self.layout.row(align=True)
|
||||
@@ -116,11 +110,6 @@ class BIM_PT_Boundary(Panel):
|
||||
else:
|
||||
row.label(text="")
|
||||
|
||||
def draw_enum_data(self, boundary, ifc_attribute: str):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=ifc_attribute)
|
||||
row.label(text=getattr(boundary, ifc_attribute, "") or "")
|
||||
|
||||
def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str):
|
||||
if hasattr(boundary, ifc_attribute):
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -46,26 +46,26 @@ def get_libraries(self, context):
|
||||
|
||||
|
||||
def get_namespaces(self, context):
|
||||
global NAMESPACES_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
global NAMESPACES_ENUM_ITEMS
|
||||
NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
|
||||
return NAMESPACES_ENUM_ITEMS
|
||||
|
||||
|
||||
def get_brick_entity_classes(self, context):
|
||||
global ENTITY_CLASSES_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
global ENTITY_CLASSES_ENUM_ITEMS
|
||||
entity = self.brick_entity_create_type
|
||||
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
|
||||
return ENTITY_CLASSES_ENUM_ITEMS
|
||||
|
||||
|
||||
def get_brick_roots(self, context):
|
||||
global BRICK_ROOTS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
global BRICK_ROOTS_ENUM_ITEMS
|
||||
BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes]
|
||||
return BRICK_ROOTS_ENUM_ITEMS
|
||||
|
||||
|
||||
def get_brick_relations(self, context):
|
||||
global BRICK_RELATIONS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
global BRICK_RELATIONS_ENUM_ITEMS
|
||||
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
|
||||
for relation in BrickschemaData.data["active_relations"]:
|
||||
if relation["predicate_name"] == "label":
|
||||
|
||||
@@ -37,7 +37,6 @@ messages = {
|
||||
class CadTrimExtend(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_trim_extend"
|
||||
bl_label = "CAD Trim / Extend"
|
||||
bl_description = "Extends/reduces element to 3D cursor"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -83,7 +82,6 @@ class CadTrimExtend(bpy.types.Operator):
|
||||
class CadMitre(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_mitre"
|
||||
bl_label = "CAD Mitre"
|
||||
bl_description = "Joins two non-parallel paths at their intersection"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -106,37 +106,23 @@ class CadTool(WorkSpaceTool):
|
||||
)
|
||||
|
||||
row = layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
|
||||
)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context)
|
||||
|
||||
elif (
|
||||
isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
|
||||
@@ -146,21 +132,15 @@ class CadTool(WorkSpaceTool):
|
||||
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
|
||||
)
|
||||
row = layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
|
||||
)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
|
||||
else:
|
||||
if (
|
||||
@@ -188,37 +168,19 @@ class CadTool(WorkSpaceTool):
|
||||
add_layout_hotkey_operator(row, "Set Gable Roof Angle", "S_R", "Set Gable Roof Angle", ui_context)
|
||||
|
||||
row = layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
|
||||
)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
|
||||
)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row,
|
||||
"2-Point Arc",
|
||||
"S_C",
|
||||
bpy.ops.bim.cad_arc_from_2_points.__doc__.split("\n", 1)[1].strip(),
|
||||
ui_context,
|
||||
)
|
||||
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row,
|
||||
"3-Point Arc",
|
||||
"S_V",
|
||||
bpy.ops.bim.cad_arc_from_3_points.__doc__.split("\n", 1)[1].strip(),
|
||||
ui_context,
|
||||
)
|
||||
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
|
||||
|
||||
|
||||
class CadHotkey(bpy.types.Operator):
|
||||
|
||||
@@ -201,10 +201,16 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
||||
"ALT+click to run a quick clash without selecting a file to save."
|
||||
)
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
|
||||
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||
quick_clash: bpy.props.BoolProperty(
|
||||
filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
default="*.bcf;*.json", options={"HIDDEN"}
|
||||
)
|
||||
format: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Format", items=[(i, i, "") for i in ("bcf", "json")]
|
||||
)
|
||||
filepath: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
subtype="FILE_PATH", options={"SKIP_SAVE"}
|
||||
)
|
||||
quick_clash: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
|
||||
@@ -37,12 +37,12 @@ from bonsai.bim.prop import BIMFilterGroup, StrProperty
|
||||
|
||||
|
||||
class ClashSource(PropertyGroup):
|
||||
name: StringProperty(
|
||||
name: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="File",
|
||||
description="Absolute filepath to existing .ifc file to use as a clash source.",
|
||||
)
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
mode: EnumProperty(
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") # pyright: ignore[reportRedeclaration]
|
||||
mode: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[
|
||||
("a", "All Elements", "All elements will be used for clashing"),
|
||||
("i", "Include", "Only the selected elements are included for clashing"),
|
||||
@@ -62,7 +62,7 @@ class Clash(PropertyGroup):
|
||||
b_global_id: StringProperty(name="B")
|
||||
a_name: StringProperty(name="A Name")
|
||||
b_name: StringProperty(name="B Name")
|
||||
clash_type: EnumProperty(
|
||||
clash_type: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Clash Type",
|
||||
items=tuple((i, i, "") for i in CLASH_TYPE_ITEMS),
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@ class CopyCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Copy Cost Schedule"
|
||||
bl_description = "Create a duplicate of the provided cost schedule."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
cost_schedule: bpy.props.IntProperty()
|
||||
cost_schedule: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
cost_schedule: int
|
||||
|
||||
@@ -260,14 +260,14 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
)
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
geometry_library: bpy.props.EnumProperty(
|
||||
geometry_library: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Geometry Library",
|
||||
description="Geometry library to use for testing shape creation.",
|
||||
items=[(i, i, "") for i in get_args(ifcopenshell.geom.GEOMETRY_LIBRARY)],
|
||||
# By default use the same library as used for importing ifc project.
|
||||
default="hybrid-cgal-simple-opencascade",
|
||||
)
|
||||
custom_geometry_library: bpy.props.StringProperty(
|
||||
custom_geometry_library: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Custom Geometry Library",
|
||||
description="Provide a custom geometry library name, will override the 'geometry library' property.",
|
||||
)
|
||||
@@ -781,7 +781,7 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Purge Unused Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
object_type: bpy.props.EnumProperty(
|
||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Object Type",
|
||||
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
||||
)
|
||||
@@ -827,7 +827,7 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
object_type: bpy.props.EnumProperty(
|
||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Object Type",
|
||||
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
||||
)
|
||||
@@ -1073,7 +1073,7 @@ class ChangeLogLevel(bpy.types.Operator):
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Change general log level across all Python code in Blender"
|
||||
|
||||
log_level: bpy.props.EnumProperty(
|
||||
log_level: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Log Level",
|
||||
items=[(i, i, "") for i in get_args(LogLevelType)],
|
||||
default="WARNING",
|
||||
|
||||
@@ -1787,7 +1787,7 @@ class CutDecorator:
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
rot_checksum_bytes: bytes = eval(DecoratorData.camera_rotation_checksum)
|
||||
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9).reshape(3, 3)
|
||||
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9)
|
||||
rot_real = tool.Blender.np_array_legacy(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))
|
||||
|
||||
@@ -1285,7 +1285,7 @@ class SnapManager:
|
||||
continue
|
||||
|
||||
coords = np.empty(vertex_count * 3, dtype=np.float32)
|
||||
mesh.vertices.foreach_get("co", coords)
|
||||
mesh.vertices.foreach_get("co", coords) # type: ignore[arg-type]
|
||||
coords = coords.reshape(-1, 3)
|
||||
|
||||
matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
|
||||
|
||||
@@ -456,8 +456,7 @@ def format_distance(
|
||||
tx_dist = fmt % d_cm
|
||||
|
||||
else:
|
||||
assert f"Unexpected unit_system - '{unit_system}'."
|
||||
# tx_dist = fmt % value
|
||||
tx_dist = fmt % value
|
||||
|
||||
return tx_dist
|
||||
|
||||
|
||||
@@ -246,17 +246,17 @@ class CreateDrawing(bpy.types.Operator):
|
||||
+ "Add the CTRL modifier to optionally open drawings to view them as\n"
|
||||
+ "they are created"
|
||||
)
|
||||
print_all: bpy.props.BoolProperty(
|
||||
print_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Print All",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
open_viewer: bpy.props.BoolProperty(
|
||||
open_viewer: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Open in Viewer",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
sync: bpy.props.BoolProperty(
|
||||
sync: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Sync Before Creating Drawing",
|
||||
description="Could save some time if you're sure IFC and current Blender session are already in sync",
|
||||
default=True,
|
||||
@@ -1426,7 +1426,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
"/Pset_.*Common/.Status",
|
||||
"EPset_Status.Status",
|
||||
"EPset_Status.UserDefinedStatus",
|
||||
"Material.Name",
|
||||
]
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
@@ -2322,14 +2321,14 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view"
|
||||
)
|
||||
|
||||
drawing: bpy.props.IntProperty()
|
||||
should_view_from_camera: bpy.props.BoolProperty(
|
||||
drawing: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
should_view_from_camera: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Should View From Camera",
|
||||
description="Move view to the activated drawing's camera position.",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
use_quick_preview: bpy.props.BoolProperty(
|
||||
use_quick_preview: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Use Quick Preview",
|
||||
description="Just move the camera to the drawing view, without loading anything else.",
|
||||
default=False,
|
||||
@@ -3306,8 +3305,9 @@ class AddTextLiteral(bpy.types.Operator):
|
||||
attr.data_type = "string"
|
||||
attr.string_value = literal_attr_values[attr_name]
|
||||
|
||||
literal_props.align_vertical = "bottom"
|
||||
literal_props.align_horizontal = "left"
|
||||
box_alignment_mask = [False] * 9
|
||||
box_alignment_mask[6] = True # bottom_left box_alignment
|
||||
literal_props.box_alignment = box_alignment_mask
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -3365,55 +3365,57 @@ class OrderTextLiteralDown(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AssignSelectedObjectAsProduct(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# Ifc Operator is unnecessary, because suboperator is handling IFC changes.
|
||||
class AssignSelectedObjectAsProduct(bpy.types.Operator):
|
||||
bl_idname = "bim.assign_selected_as_product"
|
||||
bl_label = "Assign Selected Object As Product"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if len(context.selected_objects) < 2:
|
||||
cls.poll_message_set("At least 2 objects need to be selected")
|
||||
if len(context.selected_objects) != 2:
|
||||
cls.poll_message_set("2 objects need to be selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context):
|
||||
assert bpy.context.view_layer
|
||||
objs = context.selected_objects[:]
|
||||
ifc_objs = [(o, tool.Ifc.get_entity(o)) for o in objs if tool.Ifc.get_entity(o)]
|
||||
obj1, obj2 = objs
|
||||
element1 = tool.Ifc.get_entity(obj1)
|
||||
element2 = tool.Ifc.get_entity(obj2)
|
||||
assert element1 and element2
|
||||
|
||||
annotations = [(o, e) for o, e in ifc_objs if e.is_a("IfcAnnotation")]
|
||||
non_annotations = [(o, e) for o, e in ifc_objs if not e.is_a("IfcAnnotation")]
|
||||
# Check if at least one object is an IfcAnnotation
|
||||
is_annotation1 = element1.is_a("IfcAnnotation")
|
||||
is_annotation2 = element2.is_a("IfcAnnotation")
|
||||
|
||||
if not annotations:
|
||||
self.report({"ERROR"}, "At least one selected object must be an IfcAnnotation.")
|
||||
if not (is_annotation1 or is_annotation2):
|
||||
self.report({"ERROR"}, "At least one of the selected objects must be IfcAnnotation.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if len(non_annotations) == 1:
|
||||
# One product, one or more annotations — assign all annotations to the product.
|
||||
product = non_annotations[0][1]
|
||||
elif len(non_annotations) == 0 and len(annotations) == 2:
|
||||
# Both objects are annotations — use the non-active one as the relating product.
|
||||
# If both are annotations, use the currently active object as relating product
|
||||
if is_annotation1 and is_annotation2:
|
||||
active_obj = context.active_object
|
||||
if annotations[0][0] == active_obj:
|
||||
annotation_obj, annotation = annotations[0]
|
||||
product = annotations[1][1]
|
||||
if active_obj == obj1:
|
||||
other_selected_object = obj1
|
||||
bpy.context.view_layer.objects.active = obj2
|
||||
else:
|
||||
annotation_obj, annotation = annotations[1]
|
||||
product = annotations[0][1]
|
||||
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product)
|
||||
tool.Blender.update_viewport()
|
||||
return
|
||||
other_selected_object = obj2
|
||||
bpy.context.view_layer.objects.active = obj1
|
||||
# If only one is an annotation, make it the active object
|
||||
elif is_annotation1:
|
||||
other_selected_object = obj2
|
||||
bpy.context.view_layer.objects.active = obj1
|
||||
else:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Select exactly one product object and one or more IfcAnnotation objects.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
other_selected_object = obj1
|
||||
bpy.context.view_layer.objects.active = obj2
|
||||
|
||||
for annotation_obj, _ in annotations:
|
||||
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product)
|
||||
|
||||
tool.Blender.update_viewport()
|
||||
assert (active_obj := context.active_object)
|
||||
props = tool.Drawing.get_object_assigned_product_props(active_obj)
|
||||
props.relating_product = other_selected_object
|
||||
bpy.ops.bim.edit_assigned_product()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -3635,12 +3637,14 @@ class ToggleTargetView(bpy.types.Operator):
|
||||
bl_label = "Toggle Target View"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
target_view: bpy.props.StringProperty()
|
||||
toggle_all: bpy.props.BoolProperty(
|
||||
target_view: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
toggle_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
option: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(ToggleOption)])
|
||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(ToggleOption)]
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
target_view: str
|
||||
@@ -3883,7 +3887,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
image = load_image(abs_path.name, str(abs_path.parent), check_existing=False)
|
||||
params = {"check_existing": False}
|
||||
image = load_image(abs_path.name, str(abs_path.parent), **params)
|
||||
|
||||
mesh = bpy.data.meshes.new(image_filepath.stem)
|
||||
obj = bpy.data.objects.new(image_filepath.stem, mesh)
|
||||
@@ -4173,7 +4178,10 @@ class SelectSimilarTextLiteralValue(bpy.types.Operator):
|
||||
should_select = True
|
||||
break
|
||||
elif self.attribute_type == "box_alignment":
|
||||
if literal.get_box_alignment() == self.literal_value:
|
||||
box_alignment_attr = next(
|
||||
(attr for attr in literal.attributes if attr.name == "BoxAlignment"), None
|
||||
)
|
||||
if box_alignment_attr and box_alignment_attr.string_value == self.literal_value:
|
||||
should_select = True
|
||||
break
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
BoolVectorProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
@@ -672,6 +673,20 @@ class BIMCameraProperties(PropertyGroup):
|
||||
return ortho_scale, aspect_ratio
|
||||
|
||||
|
||||
DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
|
||||
BOX_ALIGNMENT_POSITIONS = [
|
||||
"top-left",
|
||||
"top-middle",
|
||||
"top-right",
|
||||
"middle-left",
|
||||
"center",
|
||||
"middle-right",
|
||||
"bottom-left",
|
||||
"bottom-middle",
|
||||
"bottom-right",
|
||||
]
|
||||
|
||||
|
||||
class ElementValueRow(PropertyGroup):
|
||||
"""Represents a single element value row with category, key, and formatted value"""
|
||||
|
||||
@@ -774,38 +789,40 @@ def get_category_items_with_counts(self, context):
|
||||
|
||||
|
||||
class LiteralProps(PropertyGroup):
|
||||
attributes: CollectionProperty(name="Attributes", type=Attribute)
|
||||
ifc_definition_id: IntProperty(name="IFC definition ID", default=0)
|
||||
align_horizontal: EnumProperty(
|
||||
items=[
|
||||
("left", "Left", "", "ALIGN_LEFT", 0),
|
||||
("middle", "Middle", "", "ALIGN_CENTER", 1),
|
||||
("right", "Right", "", "ALIGN_RIGHT", 2),
|
||||
],
|
||||
default="left",
|
||||
name="Horizontal Alignment",
|
||||
)
|
||||
align_vertical: EnumProperty(
|
||||
items=[
|
||||
("top", "Top", "", "ALIGN_TOP", 0),
|
||||
("middle", "Middle", "", "ALIGN_MIDDLE", 1),
|
||||
("bottom", "Bottom", "", "ALIGN_BOTTOM", 2),
|
||||
],
|
||||
default="middle",
|
||||
name="Vertical Alignment",
|
||||
)
|
||||
def set_box_alignment(self, new_value):
|
||||
markers = new_value.count(True)
|
||||
if not markers:
|
||||
return
|
||||
|
||||
def get_box_alignment(self) -> str:
|
||||
alignment = self.align_vertical + "-" + self.align_horizontal
|
||||
if alignment == "middle-middle":
|
||||
alignment = "center"
|
||||
return alignment
|
||||
if markers > 1:
|
||||
prev_value = self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
|
||||
# looking for the first value changed to positive
|
||||
first_changed_value = next((i for i in range(9) if new_value[i] and new_value[i] != prev_value[i]), None)
|
||||
|
||||
# if nothing have changed we just keep the previous value
|
||||
if first_changed_value is None:
|
||||
return
|
||||
new_value = [False] * 9
|
||||
new_value[first_changed_value] = True
|
||||
|
||||
self["box_alignment"] = new_value
|
||||
position_string = BOX_ALIGNMENT_POSITIONS[next(i for i in range(9) if new_value[i])]
|
||||
self.attributes["BoxAlignment"].set_value(position_string)
|
||||
|
||||
def get_box_alignment(self):
|
||||
return self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
|
||||
|
||||
attributes: CollectionProperty(name="Attributes", type=Attribute)
|
||||
box_alignment: BoolVectorProperty(
|
||||
name="Box alignment", size=9, set=set_box_alignment, get=get_box_alignment, default=DEFAULT_BOX_ALIGNMENT
|
||||
)
|
||||
ifc_definition_id: IntProperty(name="IFC definition ID", default=0)
|
||||
|
||||
def get_literal_edited_data(self) -> dict[str, str]:
|
||||
text_data = {
|
||||
"CurrentValue": self.attributes["Literal"].string_value,
|
||||
"Literal": self.attributes["Literal"].string_value,
|
||||
"BoxAlignment": self.get_box_alignment(),
|
||||
"BoxAlignment": self.attributes["BoxAlignment"].string_value,
|
||||
}
|
||||
return text_data
|
||||
|
||||
@@ -843,30 +860,23 @@ class LiteralProps(PropertyGroup):
|
||||
if TYPE_CHECKING:
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
value: str
|
||||
box_alignment: tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]
|
||||
ifc_definition_id: int
|
||||
align_horizontal: str
|
||||
align_vertical: str
|
||||
element_value_rows: bpy.types.bpy_prop_collection_idprop[ElementValueRow]
|
||||
category_for_adding: str
|
||||
|
||||
|
||||
def update_text_alignment(self, context):
|
||||
for literal_props in self.literals:
|
||||
literal_props.align_horizontal = self.align_horizontal
|
||||
literal_props.align_vertical = self.align_vertical
|
||||
|
||||
|
||||
class BIMTextProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
literals: CollectionProperty(name="Literals", type=LiteralProps)
|
||||
newline_at: IntProperty(name="Newline At")
|
||||
symbol: EnumProperty(
|
||||
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Symbol",
|
||||
description="Symbol from symbols.svg to use for this text.",
|
||||
items=[(s, s, "") for s in ["NO SYMBOL", "CUSTOM SYMBOL"] + tool.Drawing.DEFAULT_SYMBOLS],
|
||||
default="NO SYMBOL",
|
||||
)
|
||||
custom_symbol: StringProperty(
|
||||
custom_symbol: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Custom Symbol",
|
||||
description="Non-default symbol to use for this text.",
|
||||
)
|
||||
@@ -889,7 +899,6 @@ class BIMTextProperties(PropertyGroup):
|
||||
],
|
||||
default="left",
|
||||
name="Horizontal Alignment",
|
||||
update=update_text_alignment,
|
||||
)
|
||||
align_vertical: EnumProperty(
|
||||
items=[
|
||||
@@ -899,7 +908,6 @@ class BIMTextProperties(PropertyGroup):
|
||||
],
|
||||
default="middle",
|
||||
name="Vertical Alignment",
|
||||
update=update_text_alignment,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -366,6 +366,9 @@ class BaseLinesShader(BaseShader):
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, gap_size=16):
|
||||
super().__init__(gap_size=gap_size)
|
||||
|
||||
def glenable(self):
|
||||
super().glenable()
|
||||
|
||||
|
||||
@@ -781,10 +781,33 @@ class BIM_PT_text(Panel):
|
||||
if other_attributes:
|
||||
bonsai.bim.helper.draw_attributes(other_attributes, box)
|
||||
|
||||
row = box.row()
|
||||
row.label(text="Alignment")
|
||||
row.prop(literal_props, "align_horizontal", text="", expand=True)
|
||||
row.prop(literal_props, "align_vertical", text="", expand=True)
|
||||
row = box.row(align=True)
|
||||
cols = [row.column(align=True) for j in range(3)]
|
||||
for j in range(9):
|
||||
cols[j % 3].prop(
|
||||
literal_props,
|
||||
"box_alignment",
|
||||
text="",
|
||||
index=j,
|
||||
icon="RADIOBUT_ON" if literal_props.box_alignment[j] else "RADIOBUT_OFF",
|
||||
)
|
||||
|
||||
col = row.column(align=True)
|
||||
alignment_label_row = col.row(align=True)
|
||||
alignment_label_row.label(text=" Text box alignment:")
|
||||
|
||||
box_alignment_value = (
|
||||
literal_props.attributes[
|
||||
next(
|
||||
(idx for idx, attr in enumerate(literal_props.attributes) if attr.name == "BoxAlignment"),
|
||||
-1,
|
||||
)
|
||||
].string_value
|
||||
if any(attr.name == "BoxAlignment" for attr in literal_props.attributes)
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
col.label(text=f" {box_alignment_value}")
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
@@ -816,6 +839,7 @@ class BIM_PT_text(Panel):
|
||||
|
||||
for i, literal_data in enumerate(text_data["Literals"]):
|
||||
box = self.layout.box()
|
||||
box.label(text=f"Literal[{i}]:")
|
||||
|
||||
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
||||
for attribute in literal_data:
|
||||
|
||||
@@ -85,7 +85,7 @@ class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.override_mesh_separate"
|
||||
bl_label = "IFC Mesh Separate"
|
||||
blender_op = bpy.ops.mesh.separate.get_rna_type() # ty: ignore[missing-argument]
|
||||
blender_op = bpy.ops.mesh.separate.get_rna_type()
|
||||
bl_description = blender_op.description + ".\nAlso makes sure changes are in sync with IFC."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
blender_type_prop = blender_op.properties["type"]
|
||||
@@ -246,7 +246,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.override_origin_set"
|
||||
blender_op = bpy.ops.object.origin_set.get_rna_type() # ty: ignore[missing-argument]
|
||||
blender_op = bpy.ops.object.origin_set.get_rna_type()
|
||||
bl_label = "IFC Origin Set"
|
||||
bl_description = (
|
||||
blender_op.description + ".\nAlso makes sure changes are in sync with IFC (operator works only on IFC objects)"
|
||||
@@ -801,7 +801,7 @@ def calc_delete_is_batch(ifc_file: ifcopenshell.file, context: bpy.types.Context
|
||||
class OverrideDelete(bpy.types.Operator):
|
||||
bl_idname = "bim.override_object_delete"
|
||||
bl_label = "IFC Delete"
|
||||
blender_op = bpy.ops.object.delete.get_rna_type() # ty: ignore[missing-argument]
|
||||
blender_op = bpy.ops.object.delete.get_rna_type()
|
||||
bl_description = (
|
||||
blender_op.description
|
||||
+ ".\nAlso makes sure changes in sync with IFC."
|
||||
@@ -821,7 +821,7 @@ class OverrideDelete(bpy.types.Operator):
|
||||
def poll(cls, context):
|
||||
# Match `object.delete` poll for consistency.
|
||||
# `object.delete` poll just checks for OBJECT mode.
|
||||
poll = bpy.ops.object.delete.poll() # ty: ignore[missing-argument]
|
||||
poll = bpy.ops.object.delete.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode")
|
||||
@@ -1045,7 +1045,7 @@ class SelectedIdsData(NamedTuple):
|
||||
class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.override_outliner_delete"
|
||||
bl_label = "IFC Delete"
|
||||
blender_op = bpy.ops.outliner.delete.get_rna_type() # ty: ignore[missing-argument]
|
||||
blender_op = bpy.ops.outliner.delete.get_rna_type()
|
||||
bl_description = (
|
||||
blender_op.description
|
||||
+ ".\nAlso makes sure changes in sync with IFC."
|
||||
@@ -1060,13 +1060,13 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def poll(cls, context) -> bool:
|
||||
# Match `outliner.delete` poll for consistency.
|
||||
# `outliner.delete` just checks `area.type` == `OUTLINER`.
|
||||
poll = bpy.ops.outliner.delete.poll() # ty: ignore[missing-argument]
|
||||
poll = bpy.ops.outliner.delete.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available from Outliner.")
|
||||
return False
|
||||
|
||||
def execute(self, context): # ty:ignore[override-of-final-method]
|
||||
def execute(self, context):
|
||||
if len(getattr(context, "selected_ids", [])) == 0:
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1164,7 +1164,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
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() # ty: ignore[missing-argument]
|
||||
poll = bpy.ops.object.duplicate_move.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode")
|
||||
@@ -1908,7 +1908,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.override_object_join"
|
||||
bl_label = "IFC Join"
|
||||
blender_op = bpy.ops.mesh.separate.get_rna_type() # ty: ignore[missing-argument]
|
||||
blender_op = bpy.ops.mesh.separate.get_rna_type()
|
||||
bl_description = (
|
||||
blender_op.description
|
||||
+ ".\nAlso makes sure changes are in sync with IFC."
|
||||
@@ -1926,7 +1926,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not bpy.ops.object.join.poll(): # ty: ignore[missing-argument]
|
||||
if not bpy.ops.object.join.poll():
|
||||
cls.poll_message_set("Active object is not EDITable.")
|
||||
return False
|
||||
if not context.selected_editable_objects:
|
||||
@@ -2289,7 +2289,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif obj in pprops.clipping_planes_objs:
|
||||
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
||||
elif element:
|
||||
if not obj.data or obj.type not in ("MESH", "CURVE"):
|
||||
if not obj.data:
|
||||
self.report({"INFO"}, "No geometry to edit")
|
||||
elif tool.Geometry.is_locked(element):
|
||||
self.report({"ERROR"}, lock_error_message(obj.name))
|
||||
|
||||
@@ -139,9 +139,7 @@ def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.typ
|
||||
tool.Georeference.set_coordinates(
|
||||
"blender",
|
||||
ifcopenshell.util.geolocation.enh2xyz(
|
||||
local_coordinates[0],
|
||||
local_coordinates[1],
|
||||
local_coordinates[2],
|
||||
*local_coordinates,
|
||||
float(props.blender_offset_x),
|
||||
float(props.blender_offset_y),
|
||||
float(props.blender_offset_z),
|
||||
@@ -164,9 +162,7 @@ def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types
|
||||
tool.Georeference.set_coordinates(
|
||||
"blender",
|
||||
ifcopenshell.util.geolocation.enh2xyz(
|
||||
local_coordinates[0],
|
||||
local_coordinates[1],
|
||||
local_coordinates[2],
|
||||
*local_coordinates,
|
||||
float(props.blender_offset_x),
|
||||
float(props.blender_offset_y),
|
||||
float(props.blender_offset_z),
|
||||
@@ -271,8 +267,6 @@ class BIMGeoreferenceProperties(PropertyGroup):
|
||||
x_axis_ordinate: str
|
||||
x_axis_is_null: bool
|
||||
|
||||
model_is_georeferenced: bool
|
||||
model_crs: str
|
||||
model_origin: str
|
||||
model_origin_si: str
|
||||
model_project_north: str
|
||||
|
||||
@@ -27,7 +27,7 @@ from bonsai.bim.prop import StrProperty
|
||||
|
||||
class BIMCityJsonProperties(PropertyGroup):
|
||||
def get_lods(self, context):
|
||||
global LODS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
global LODS_ENUM_ITEMS
|
||||
LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
|
||||
return LODS_ENUM_ITEMS
|
||||
|
||||
|
||||
@@ -43,11 +43,11 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Toggle Group"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
ifc_definition_id: bpy.props.IntProperty()
|
||||
group_type: bpy.props.EnumProperty(
|
||||
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
group_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(tool.Group.GroupType)],
|
||||
)
|
||||
option: bpy.props.EnumProperty(
|
||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(tool.Group.ToggleOption)],
|
||||
)
|
||||
|
||||
|
||||
@@ -34,10 +34,8 @@ classes = (
|
||||
operator.Fetch,
|
||||
operator.Merge,
|
||||
operator.ObjectLog,
|
||||
operator.SelectConflictEntity,
|
||||
operator.Push,
|
||||
operator.RefreshGit,
|
||||
operator.RenameBranch,
|
||||
operator.SwitchRevision,
|
||||
operator.InstallGit,
|
||||
operator.RunGitDiff,
|
||||
|
||||
@@ -21,71 +21,65 @@ class IfcGitData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
repo = None
|
||||
if bool(tool.Ifc.get()):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
repo = tool.IfcGit.repo_from_path(path_ifc)
|
||||
|
||||
cls.data = {
|
||||
"repo": repo,
|
||||
"remotes": repo.remotes if repo else None,
|
||||
"branch_names": cls.branch_names(repo),
|
||||
"tag_names": cls.tag_names(repo),
|
||||
"remote_names": cls.remote_names(repo),
|
||||
"remote_urls": {r.name: r.url for r in repo.remotes} if repo else {},
|
||||
"repo": cls.repo(),
|
||||
"remotes": cls.remotes(),
|
||||
"branch_names": cls.branch_names(),
|
||||
"remote_names": cls.remote_names(),
|
||||
"remote_urls": cls.remote_urls(),
|
||||
"path_ifc": cls.path_ifc(),
|
||||
"branches_by_hexsha": cls.branches_by_hexsha(),
|
||||
"tags_by_hexsha": cls.tags_by_hexsha(),
|
||||
"name_ifc": cls.name_ifc(repo),
|
||||
"name_ifc": cls.name_ifc(),
|
||||
"dir_name": cls.dir_name(),
|
||||
"base_name": cls.base_name(),
|
||||
"working_dir": repo.working_dir if repo else None,
|
||||
"ifc_is_untracked": cls.ifc_is_untracked(repo),
|
||||
"is_detached": repo.head.is_detached if repo else None,
|
||||
"active_branch_name": repo.active_branch.name if repo and not repo.head.is_detached else None,
|
||||
"is_dirty": cls.is_dirty(repo),
|
||||
"current_revision": cls.current_revision(repo),
|
||||
"working_dir": cls.working_dir(),
|
||||
"untracked_files": cls.untracked_files(),
|
||||
"is_detached": cls.is_detached(),
|
||||
"active_branch_name": cls.active_branch_name(),
|
||||
"is_dirty": cls.is_dirty(),
|
||||
"commit": cls.commit(),
|
||||
"current_revision": cls.current_revision(),
|
||||
"git_exe": cls.git_exe(),
|
||||
"ifcmerge_exe": cls.ifcmerge_exe(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def branch_names(cls, repo):
|
||||
if not repo or not repo.heads:
|
||||
return []
|
||||
names = sorted([b.name for b in repo.branches])
|
||||
if "main" in names:
|
||||
names.remove("main")
|
||||
names = ["main"] + names
|
||||
if repo.remotes:
|
||||
for remote in repo.remotes:
|
||||
for ref in remote.refs:
|
||||
names.append(ref.name)
|
||||
return names
|
||||
def repo(cls):
|
||||
if bool(tool.Ifc.get()):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return tool.IfcGit.repo_from_path(path_ifc)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def tag_names(cls, repo):
|
||||
if not repo:
|
||||
return []
|
||||
return [t.name for t in repo.tags]
|
||||
def remotes(cls):
|
||||
if cls.repo():
|
||||
return cls.repo().remotes
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def remote_names(cls, repo):
|
||||
if not repo:
|
||||
return []
|
||||
names = sorted([r.name for r in repo.remotes])
|
||||
if "origin" in names:
|
||||
names.remove("origin")
|
||||
names = ["origin"] + names
|
||||
return names
|
||||
def branch_names(cls):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def remote_names(cls):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def remote_urls(cls):
|
||||
result = {}
|
||||
if cls.repo():
|
||||
for remote in cls.repo().remotes:
|
||||
result[remote.name] = remote.url
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def path_ifc(cls):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return path_ifc
|
||||
return tool.Ifc.get_path()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@@ -94,8 +88,7 @@ class IfcGitData:
|
||||
if tool.IfcGitRepo.repo.branches:
|
||||
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
|
||||
except AttributeError:
|
||||
pass
|
||||
return {}
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def tags_by_hexsha(cls):
|
||||
@@ -104,11 +97,12 @@ class IfcGitData:
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def name_ifc(cls, repo):
|
||||
if bool(tool.Ifc.get()) and repo:
|
||||
def name_ifc(cls):
|
||||
if bool(tool.Ifc.get()):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return os.path.relpath(path_ifc, repo.working_dir)
|
||||
if tool.IfcGitRepo.repo and os.path.isfile(path_ifc):
|
||||
working_dir = tool.IfcGitRepo.repo.working_dir
|
||||
return os.path.relpath(path_ifc, working_dir)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@@ -128,28 +122,49 @@ class IfcGitData:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def ifc_is_untracked(cls, repo):
|
||||
"""Return True if the IFC file exists in the repo but has not been added to git."""
|
||||
if not repo:
|
||||
return False
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if not os.path.isfile(path_ifc):
|
||||
return False
|
||||
return not bool(repo.git.ls_files(path_ifc))
|
||||
def working_dir(cls):
|
||||
if cls.repo():
|
||||
return cls.repo().working_dir
|
||||
|
||||
@classmethod
|
||||
def is_dirty(cls, repo):
|
||||
if repo and cls.git_exe():
|
||||
def untracked_files(cls):
|
||||
if cls.repo():
|
||||
return cls.repo().untracked_files
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def is_detached(cls):
|
||||
if cls.repo():
|
||||
return cls.repo().head.is_detached
|
||||
|
||||
@classmethod
|
||||
def active_branch_name(cls):
|
||||
if cls.repo() and not cls.is_detached():
|
||||
return cls.repo().active_branch.name
|
||||
|
||||
@classmethod
|
||||
def is_dirty(cls):
|
||||
if cls.repo() and cls.git_exe():
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return repo.is_dirty(path=path_ifc)
|
||||
return cls.repo().is_dirty(path=path_ifc)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def current_revision(cls, repo):
|
||||
def commit(cls):
|
||||
props = tool.IfcGit.get_ifcgit_props()
|
||||
if repo and repo.head.is_valid() and len(props.ifcgit_commits) > 0:
|
||||
return repo.commit()
|
||||
if cls.repo() and len(props.ifcgit_commits) > 0:
|
||||
item = props.ifcgit_commits[props.commit_index]
|
||||
try:
|
||||
return cls.repo().commit(rev=item.hexsha)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def current_revision(cls):
|
||||
props = tool.IfcGit.get_ifcgit_props()
|
||||
if cls.repo() and cls.repo().head.is_valid() and len(props.ifcgit_commits) > 0:
|
||||
return tool.IfcGitRepo.repo.commit()
|
||||
|
||||
@classmethod
|
||||
def git_exe(cls):
|
||||
|
||||
@@ -120,11 +120,11 @@ class CommitChanges(bpy.types.Operator):
|
||||
if props.commit_message == "":
|
||||
return False
|
||||
if repo:
|
||||
if props.new_branch_name in IfcGitData.data["branch_names"]:
|
||||
if props.new_branch_name in [branch.name for branch in repo.branches]:
|
||||
cls.poll_message_set("Branch already exists!")
|
||||
return False
|
||||
elif not tool.IfcGit.is_valid_ref_format(props.new_branch_name):
|
||||
if IfcGitData.data["is_detached"]:
|
||||
if repo.head.is_detached:
|
||||
cls.poll_message_set("Branch name is invalid or empty!")
|
||||
return False
|
||||
elif props.new_branch_name != "":
|
||||
@@ -134,17 +134,10 @@ class CommitChanges(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
props = tool.IfcGit.get_ifcgit_props()
|
||||
commit_message = props.commit_message
|
||||
new_branch_name = props.new_branch_name
|
||||
core.commit_changes(tool.IfcGit, tool.Ifc, commit_message, new_branch_name)
|
||||
props.new_branch_name = ""
|
||||
props.commit_message = ""
|
||||
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||
repo = IfcGitData.data["repo"]
|
||||
core.commit_changes(tool.IfcGit, tool.Ifc, repo)
|
||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
||||
refresh()
|
||||
IfcGitData.load()
|
||||
if new_branch_name:
|
||||
props.display_branch = new_branch_name
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -164,7 +157,7 @@ class AddTag(bpy.types.Operator):
|
||||
repo = IfcGitData.data["repo"]
|
||||
if repo and (
|
||||
not tool.IfcGit.is_valid_ref_format(props.new_tag_name)
|
||||
or props.new_tag_name in IfcGitData.data["tag_names"]
|
||||
or props.new_tag_name in [tag.name for tag in repo.tags]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
@@ -172,12 +165,8 @@ class AddTag(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
|
||||
repo = IfcGitData.data["repo"]
|
||||
props = tool.IfcGit.get_ifcgit_props()
|
||||
item = props.ifcgit_commits[props.commit_index]
|
||||
core.add_tag(tool.IfcGit, repo, item.hexsha, props.new_tag_name, props.new_tag_message)
|
||||
props.new_tag_name = ""
|
||||
props.new_tag_message = ""
|
||||
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||
core.add_tag(tool.IfcGit, repo)
|
||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -194,7 +183,7 @@ class DeleteTag(bpy.types.Operator):
|
||||
|
||||
repo = IfcGitData.data["repo"]
|
||||
core.delete_tag(tool.IfcGit, repo, self.tag_name)
|
||||
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -202,7 +191,7 @@ class DeleteTag(bpy.types.Operator):
|
||||
class RefreshGit(bpy.types.Operator):
|
||||
"""Refresh revision list"""
|
||||
|
||||
bl_label = "Refresh"
|
||||
bl_label = ""
|
||||
bl_idname = "ifcgit.refresh"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@@ -216,7 +205,8 @@ class RefreshGit(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||
repo = IfcGitData.data["repo"]
|
||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
||||
refresh()
|
||||
tool.IfcGit.decolourise()
|
||||
return {"FINISHED"}
|
||||
@@ -225,7 +215,7 @@ class RefreshGit(bpy.types.Operator):
|
||||
class DisplayRevision(bpy.types.Operator):
|
||||
"""Colourise objects by selected revision"""
|
||||
|
||||
bl_label = "Colourise Revision"
|
||||
bl_label = ""
|
||||
bl_idname = "ifcgit.display_revision"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@@ -260,7 +250,7 @@ class DisplayUncommitted(bpy.types.Operator):
|
||||
class SwitchRevision(bpy.types.Operator):
|
||||
"""Switches the repository to the selected revision and reloads the IFC file"""
|
||||
|
||||
bl_label = "Switch Revision"
|
||||
bl_label = ""
|
||||
bl_idname = "ifcgit.switch_revision"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@@ -278,7 +268,7 @@ class SwitchRevision(bpy.types.Operator):
|
||||
|
||||
|
||||
class Merge(bpy.types.Operator):
|
||||
"""Merges the selected branch into working branch.\nCtrl+click to preview without merging"""
|
||||
"""Merges the selected branch into working branch"""
|
||||
|
||||
bl_label = "Merge this branch"
|
||||
bl_idname = "ifcgit.merge"
|
||||
@@ -292,84 +282,15 @@ class Merge(bpy.types.Operator):
|
||||
return True
|
||||
return False
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.ctrl:
|
||||
core.dry_run_merge(tool.IfcGit, tool.Ifc, self)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
if core.merge_branch(tool.IfcGit, tool.Ifc, self) is not False:
|
||||
|
||||
if core.merge_branch(tool.IfcGit, tool.Ifc, self):
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
else:
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class SelectConflictEntity(bpy.types.Operator):
|
||||
"""Select the conflicting entity in the viewport"""
|
||||
|
||||
bl_label = "Select Conflict Entity"
|
||||
bl_idname = "ifcgit.select_conflict_entity"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
step_id: bpy.props.IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
step_id: int
|
||||
|
||||
def execute(self, context):
|
||||
model = tool.Ifc.get()
|
||||
if not model:
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
entity = model.by_id(self.step_id)
|
||||
except Exception:
|
||||
self.report({"WARNING"}, f"Entity #{self.step_id} not found (may have been deleted locally)")
|
||||
return {"CANCELLED"}
|
||||
|
||||
obj = tool.Ifc.get_object(entity)
|
||||
if obj is None:
|
||||
# Walk inverse references up to 5 hops to find nearest entity with a Blender object
|
||||
visited = {entity.id()}
|
||||
queue = [entity]
|
||||
for _ in range(5):
|
||||
next_queue = []
|
||||
for ent in queue:
|
||||
for inv in model.get_inverse(ent):
|
||||
if inv.id() in visited:
|
||||
continue
|
||||
visited.add(inv.id())
|
||||
obj = tool.Ifc.get_object(inv)
|
||||
if obj is not None:
|
||||
break
|
||||
next_queue.append(inv)
|
||||
if obj is not None:
|
||||
break
|
||||
if obj is not None:
|
||||
break
|
||||
queue = next_queue
|
||||
|
||||
if obj is None:
|
||||
self.report({"INFO"}, f"No viewport representation found for #{self.step_id} ({entity.is_a()})")
|
||||
return {"CANCELLED"}
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
region = next((r for r in area.regions if r.type == "WINDOW"), None)
|
||||
if region:
|
||||
with context.temp_override(area=area, region=region):
|
||||
bpy.ops.view3d.view_selected()
|
||||
break
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Push(bpy.types.Operator):
|
||||
"""Pushes the working branch to selected remote"""
|
||||
|
||||
@@ -393,9 +314,9 @@ class Fetch(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.IfcGit.get_ifcgit_props()
|
||||
core.fetch(tool.IfcGit, props.select_remote)
|
||||
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||
refresh()
|
||||
repo = IfcGitData.data["repo"]
|
||||
remote = repo.remotes[props.select_remote]
|
||||
remote.fetch()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -415,7 +336,7 @@ class AddRemote(bpy.types.Operator):
|
||||
not repo
|
||||
or not tool.IfcGit.is_valid_ref_format(props.remote_name)
|
||||
or not props.remote_url
|
||||
or props.remote_name in IfcGitData.data["remote_names"]
|
||||
or props.remote_name in [remote.name for remote in repo.remotes]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
@@ -423,11 +344,8 @@ class AddRemote(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
|
||||
repo = IfcGitData.data["repo"]
|
||||
props = tool.IfcGit.get_ifcgit_props()
|
||||
core.add_remote(tool.IfcGit, repo, props.remote_name, props.remote_url)
|
||||
props.remote_name = ""
|
||||
props.remote_url = ""
|
||||
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||
core.add_remote(tool.IfcGit, repo)
|
||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -442,19 +360,8 @@ class DeleteRemote(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
|
||||
repo = IfcGitData.data["repo"]
|
||||
props = tool.IfcGit.get_ifcgit_props()
|
||||
remote_name = props.select_remote
|
||||
if props.display_branch.startswith(remote_name + "/"):
|
||||
active = IfcGitData.data["active_branch_name"]
|
||||
if active:
|
||||
props.display_branch = active
|
||||
else:
|
||||
local_branches = [b for b in IfcGitData.data["branch_names"] if "/" not in b]
|
||||
if local_branches:
|
||||
props.display_branch = local_branches[0]
|
||||
core.delete_remote(tool.IfcGit, repo, remote_name)
|
||||
tool.IfcGit.select_first_remote()
|
||||
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||
core.delete_remote(tool.IfcGit, repo)
|
||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -468,8 +375,8 @@ class ObjectLog(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not (obj := context.active_object) or not obj.select_get():
|
||||
cls.poll_message_set("No selected object")
|
||||
if not (obj := context.active_object):
|
||||
cls.poll_message_set("No Active Object")
|
||||
elif not tool.Blender.get_ifc_definition_id(obj):
|
||||
cls.poll_message_set("Active Object doesn't have an IFC definition")
|
||||
else:
|
||||
@@ -515,7 +422,7 @@ class RunGitDiff(bpy.types.Operator):
|
||||
)
|
||||
bl_options = set()
|
||||
|
||||
save_to_temp: bpy.props.BoolProperty(options={"SKIP_SAVE"})
|
||||
save_to_temp: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
save_to_temp: bool
|
||||
@@ -538,37 +445,3 @@ class RunGitDiff(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
core.run_git_diff(tool.IfcGit, self, self.save_to_temp)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RenameBranch(bpy.types.Operator):
|
||||
"""Rename the current branch"""
|
||||
|
||||
bl_label = "Rename Branch"
|
||||
bl_idname = "ifcgit.rename_branch"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
new_name: bpy.props.StringProperty(name="New name")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
new_name: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
IfcGitData.make_sure_is_loaded()
|
||||
if not IfcGitData.data["repo"]:
|
||||
return False
|
||||
if IfcGitData.data["is_detached"]:
|
||||
return False
|
||||
if IfcGitData.data["is_dirty"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.new_name = IfcGitData.data["active_branch_name"]
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
repo = IfcGitData.data["repo"]
|
||||
core.rename_branch(tool.IfcGit, repo, self.new_name)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -17,14 +17,28 @@ from bonsai.bim.module.ifcgit.data import IfcGitData
|
||||
def git_branches(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
# NOTE "Python must keep a reference to the strings returned by
|
||||
# the callback or Blender will misbehave or even crash"
|
||||
# Branch list (local + remote, main first) is computed once in IfcGitData.load()
|
||||
IfcGitData.make_sure_is_loaded()
|
||||
return [(name, name, name) for name in IfcGitData.data["branch_names"]]
|
||||
IfcGitData.data["branch_names"] = sorted([branch.name for branch in IfcGitData.data["repo"].heads])
|
||||
|
||||
if "main" in IfcGitData.data["branch_names"]:
|
||||
IfcGitData.data["branch_names"].remove("main")
|
||||
IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"]
|
||||
|
||||
if IfcGitData.data["remotes"]:
|
||||
for remote in IfcGitData.data["remotes"]:
|
||||
for remote_branch in remote.refs:
|
||||
IfcGitData.data["branch_names"].append(remote_branch.name)
|
||||
|
||||
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
|
||||
|
||||
|
||||
def git_remotes(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
IfcGitData.make_sure_is_loaded()
|
||||
return [(name, name, name) for name in IfcGitData.data["remote_names"]]
|
||||
IfcGitData.data["remote_names"] = sorted([remote.name for remote in IfcGitData.data["remotes"]])
|
||||
|
||||
if "origin" in IfcGitData.data["remote_names"]:
|
||||
IfcGitData.data["remote_names"].remove("origin")
|
||||
IfcGitData.data["remote_names"] = ["origin"] + IfcGitData.data["remote_names"]
|
||||
|
||||
return [(myname, myname, myname) for myname in IfcGitData.data["remote_names"]]
|
||||
|
||||
|
||||
def update_revlist(self: "IfcGitProperties", context: bpy.types.Context) -> None:
|
||||
@@ -76,7 +90,6 @@ class IfcGitListItem(PropertyGroup):
|
||||
name="Commit Message",
|
||||
default="",
|
||||
)
|
||||
committed_date: IntProperty(name="Committed Date", default=0)
|
||||
tags: CollectionProperty(type=IfcGitTag, name="List of revision tags")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -85,7 +98,6 @@ class IfcGitListItem(PropertyGroup):
|
||||
author_name: str
|
||||
author_email: str
|
||||
message: str
|
||||
committed_date: int
|
||||
tags: bpy.types.bpy_prop_collection_idprop[IfcGitTag]
|
||||
|
||||
|
||||
@@ -139,11 +151,6 @@ class IfcGitProperties(PropertyGroup):
|
||||
],
|
||||
update=update_revlist,
|
||||
)
|
||||
merge_conflicts: StringProperty(
|
||||
name="Merge Conflicts",
|
||||
description="JSON report from last failed merge attempt",
|
||||
default="",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ifcgit_commits: bpy.types.bpy_prop_collection_idprop[IfcGitListItem]
|
||||
@@ -158,4 +165,3 @@ class IfcGitProperties(PropertyGroup):
|
||||
display_branch: str
|
||||
select_remote: str
|
||||
ifcgit_filter: Literal["all", "tagged", "relevant"]
|
||||
merge_conflicts: str
|
||||
|
||||
@@ -52,7 +52,7 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
||||
if IfcGitData.data["repo"] and os.path.exists(IfcGitData.data["repo"].git_dir):
|
||||
name_ifc = IfcGitData.data["name_ifc"]
|
||||
row.label(text=IfcGitData.data["working_dir"], icon="SYSTEM")
|
||||
if IfcGitData.data["ifc_is_untracked"]:
|
||||
if name_ifc in IfcGitData.data["untracked_files"]:
|
||||
row.operator(
|
||||
"ifcgit.addfile",
|
||||
text="Add '" + name_ifc + "' to repository",
|
||||
@@ -112,13 +112,15 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
||||
row.label(text="Working branch: Detached HEAD")
|
||||
else:
|
||||
row.label(text="Working branch: " + IfcGitData.data["active_branch_name"])
|
||||
row.operator("ifcgit.rename_branch", icon="GREASEPENCIL", text="")
|
||||
|
||||
row = layout.row()
|
||||
grouped = layout.row()
|
||||
column = grouped.column()
|
||||
row = column.row()
|
||||
row.prop(props, "display_branch", text="Browse branch")
|
||||
row.prop(props, "ifcgit_filter", text="Filter revisions")
|
||||
|
||||
layout.template_list(
|
||||
row = column.row()
|
||||
row.template_list(
|
||||
"COMMIT_UL_List",
|
||||
"The_List",
|
||||
props,
|
||||
@@ -126,64 +128,20 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
||||
props,
|
||||
"commit_index",
|
||||
)
|
||||
|
||||
row = layout.row(align=True)
|
||||
column = grouped.column()
|
||||
row = column.row()
|
||||
row.operator("ifcgit.refresh", icon="FILE_REFRESH")
|
||||
|
||||
if not is_dirty:
|
||||
|
||||
row = column.row()
|
||||
row.operator("ifcgit.display_revision", icon="SELECT_DIFFERENCE")
|
||||
|
||||
row = column.row()
|
||||
row.operator("ifcgit.switch_revision", icon="CURRENT_FILE")
|
||||
row.operator("ifcgit.merge", icon="SYSTEM")
|
||||
|
||||
conflicts = tool.IfcGit.get_merge_conflicts()
|
||||
if conflicts is not None:
|
||||
box = layout.box()
|
||||
box.alert = True
|
||||
row = box.row()
|
||||
row.label(
|
||||
text=f"Merge failed \u2014 {len(conflicts)} conflict(s)",
|
||||
icon="ERROR",
|
||||
)
|
||||
for conflict in conflicts:
|
||||
col = box.column(align=True)
|
||||
conflict_type = conflict.get("type", "")
|
||||
entity_id = conflict.get("entity_id", "?")
|
||||
local_id = conflict.get("original_local_id")
|
||||
|
||||
if conflict_type == "attribute_conflict":
|
||||
entity_class = conflict.get("entity_class", "Entity")
|
||||
attr_idx = conflict.get("attribute_index", "?")
|
||||
desc = f"#{entity_id} {entity_class}: attribute {attr_idx} conflict"
|
||||
elif conflict_type == "entity_deleted_and_modified":
|
||||
entity_class = conflict.get("entity_class", "Entity")
|
||||
desc = f"#{entity_id} {entity_class}: " + conflict.get("message", "deleted/modified conflict")
|
||||
elif conflict_type == "class_changed":
|
||||
desc = (
|
||||
f"#{entity_id}: class changed "
|
||||
+ conflict.get("base_class", "?")
|
||||
+ " \u2192 "
|
||||
+ conflict.get("modified_class", "?")
|
||||
)
|
||||
elif conflict_type == "required_entity_deleted":
|
||||
desc = f"#{entity_id}: " + conflict.get("message", "required entity deleted")
|
||||
else:
|
||||
desc = f"#{entity_id}: {conflict_type}"
|
||||
|
||||
row = col.row(align=True)
|
||||
row.label(text=desc)
|
||||
if local_id:
|
||||
op = row.operator(
|
||||
"ifcgit.select_conflict_entity",
|
||||
text="",
|
||||
icon="RESTRICT_SELECT_OFF",
|
||||
)
|
||||
op.step_id = local_id
|
||||
|
||||
if conflict_type == "attribute_conflict":
|
||||
sub = col.column(align=True)
|
||||
sub.scale_y = 0.75
|
||||
sub.label(text=f" Base: {conflict.get('base_value', '')}")
|
||||
sub.label(text=f" Local: {conflict.get('local_value', '')}")
|
||||
sub.label(text=f" Remote: {conflict.get('remote_value', '')}")
|
||||
row = column.row()
|
||||
row.operator("ifcgit.merge", icon="EXPERIMENTAL", text="")
|
||||
|
||||
if not props.ifcgit_commits:
|
||||
return
|
||||
@@ -258,7 +216,13 @@ class COMMIT_UL_List(bpy.types.UIList):
|
||||
):
|
||||
|
||||
current_revision = IfcGitData.data["current_revision"]
|
||||
current_hexsha = current_revision.hexsha if current_revision else None
|
||||
|
||||
# TODO Figure how this "item" can be acesse in "data.py"
|
||||
# so it's possible to move the ".commit"
|
||||
try:
|
||||
commit = IfcGitData.data["repo"].commit(rev=item.hexsha)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
lookup = IfcGitData.data["branches_by_hexsha"]
|
||||
refs = ""
|
||||
@@ -272,11 +236,11 @@ class COMMIT_UL_List(bpy.types.UIList):
|
||||
for tag in lookup[item.hexsha]:
|
||||
refs += "{" + tag.name + "} "
|
||||
|
||||
if item.hexsha == current_hexsha:
|
||||
layout.label(text="[HEAD] " + refs + item.message.split("\n")[0], icon="DECORATE_KEYFRAME")
|
||||
if commit == current_revision:
|
||||
layout.label(text="[HEAD] " + refs + commit.message.split("\n")[0], icon="DECORATE_KEYFRAME")
|
||||
else:
|
||||
layout.label(text=refs + item.message.split("\n")[0], icon="DECORATE_ANIMATE")
|
||||
layout.label(text=time.strftime("%c", time.localtime(item.committed_date)))
|
||||
layout.label(text=refs + commit.message.split("\n")[0], icon="DECORATE_ANIMATE")
|
||||
layout.label(text=time.strftime("%c", time.localtime(commit.committed_date)))
|
||||
|
||||
def draw_filter(self, context, layout):
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from pathlib import Path
|
||||
import bpy
|
||||
import pyradiance
|
||||
|
||||
from . import export, ies, list, material, prepare, prop, render, solar, ui
|
||||
from . import list, operator, prop, ui
|
||||
|
||||
|
||||
def get_pyradiance_path():
|
||||
@@ -31,43 +31,31 @@ def get_pyradiance_path():
|
||||
|
||||
|
||||
classes = (
|
||||
export.ExportOBJ,
|
||||
solar.ImportLatLong,
|
||||
solar.ImportTrueNorth,
|
||||
solar.MoveSunPathTo3DCursor,
|
||||
render.RadianceRender,
|
||||
render.FalseColorRadiance,
|
||||
render.RADIANCE_OT_select_camera,
|
||||
solar.ViewFromSun,
|
||||
solar.LightPickCoordinates,
|
||||
solar.LightSetTimeToNow,
|
||||
material.RefreshIFCMaterials,
|
||||
material.UnmapMaterial,
|
||||
material.RADIANCE_OT_export_material_mappings,
|
||||
material.RADIANCE_OT_import_material_mappings,
|
||||
material.RADIANCE_OT_open_spectraldb,
|
||||
prepare.PrepareRadianceScene,
|
||||
ies.AddIESLight,
|
||||
ies.RemoveIESLight,
|
||||
export.CleanupRadianceFiles,
|
||||
operator.ExportOBJ,
|
||||
operator.ImportLatLong,
|
||||
operator.ImportTrueNorth,
|
||||
operator.MoveSunPathTo3DCursor,
|
||||
operator.RadianceRender,
|
||||
operator.ViewFromSun,
|
||||
operator.LightPickCoordinates,
|
||||
operator.LightSetTimeToNow,
|
||||
operator.RefreshIFCMaterials,
|
||||
operator.UnmapMaterial,
|
||||
operator.RADIANCE_OT_select_camera,
|
||||
operator.RADIANCE_OT_export_material_mappings,
|
||||
operator.RADIANCE_OT_import_material_mappings,
|
||||
operator.RADIANCE_OT_open_spectraldb,
|
||||
prop.RadianceMaterial,
|
||||
prop.IESLight,
|
||||
prop.BIMSolarProperties,
|
||||
prop.RadianceExporterProperties,
|
||||
ui.BIM_PT_radiance_exporter,
|
||||
ui.BIM_PT_radiance_scene_setup,
|
||||
ui.BIM_PT_radiance_materials,
|
||||
ui.BIM_PT_radiance_lighting,
|
||||
ui.BIM_PT_radiance_render_settings,
|
||||
ui.BIM_PT_radiance_pipeline,
|
||||
ui.BIM_PT_solar,
|
||||
list.MATERIAL_UL_radiance_materials,
|
||||
list.MATERIAL_UL_ies_lights,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMRadianceExporterProperties = bpy.props.PointerProperty(type=prop.RadianceExporterProperties)
|
||||
bpy.types.Scene.BIMRadianceExporeterProperies = bpy.props.PointerProperty(type=prop.RadianceExporterProperties)
|
||||
bpy.types.Scene.BIMSolarProperties = bpy.props.PointerProperty(type=prop.BIMSolarProperties)
|
||||
|
||||
if pyradiance:
|
||||
@@ -80,5 +68,5 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMRadianceExporterProperties
|
||||
del bpy.types.Scene.BIMRadianceExporeterProperies
|
||||
del bpy.types.Scene.BIMSolarProperties
|
||||
|
||||
@@ -1,488 +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/>.
|
||||
|
||||
import multiprocessing
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.light.shared import ifc_materials, linked_model_exports
|
||||
|
||||
|
||||
class ExportOBJ(bpy.types.Operator):
|
||||
"""Exports the IFC File to OBJ"""
|
||||
|
||||
bl_idname = "export_scene.radiance"
|
||||
bl_label = "Export"
|
||||
bl_description = "Export the IFC to OBJ"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC file loaded in Bonsai.")
|
||||
return False
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
if not props.output_dir:
|
||||
cls.poll_message_set("Output directory is not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_geom_settings(self):
|
||||
"""Create standard geometry and serializer settings for OBJ export."""
|
||||
settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS)
|
||||
settings.set("apply-default-materials", True)
|
||||
serializer_settings.set("use-element-guids", True)
|
||||
settings.set("use-world-coords", True)
|
||||
return settings, serializer_settings
|
||||
|
||||
def _get_exportable_elements(self, ifc_file, filter_visibility=True):
|
||||
"""Get the list of elements to export from an IFC file."""
|
||||
if ifc_file.schema in ("IFC2X3", "IFC4"):
|
||||
elements = ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcProxy")
|
||||
else:
|
||||
elements = ifc_file.by_type("IfcElement")
|
||||
|
||||
elements += ifc_file.by_type("IfcSite")
|
||||
elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
|
||||
|
||||
if not filter_visibility:
|
||||
return elements
|
||||
|
||||
# Filter by visibility in Blender.
|
||||
# We use hide_get() (user-toggled eye icon) instead of visible_get()
|
||||
# because visible_get() also considers collection/view-layer visibility
|
||||
# which incorrectly excludes objects in linked aggregate sub-collections.
|
||||
visible_elements = []
|
||||
for element in elements:
|
||||
blender_obj = tool.Ifc.get_object(element)
|
||||
if blender_obj is None:
|
||||
# No Blender object (linked aggregate copies, or not yet represented)
|
||||
# Include by default — the geometry exists in the IFC file
|
||||
visible_elements.append(element)
|
||||
continue
|
||||
if not blender_obj.hide_get():
|
||||
visible_elements.append(element)
|
||||
else:
|
||||
print(f"Skipping hidden element: {element.GlobalId if hasattr(element, 'GlobalId') else element.id()}")
|
||||
return visible_elements
|
||||
|
||||
def _export_ifc_to_obj(self, ifc_file, obj_path, mtl_path, settings, serializer_settings, elements):
|
||||
"""Export elements from an IFC file to OBJ format. Returns collected material names."""
|
||||
materials_collected = []
|
||||
serialiser = ifcopenshell.geom.serializers.obj(obj_path, mtl_path, settings, serializer_settings)
|
||||
serialiser.setFile(ifc_file)
|
||||
serialiser.setUnitNameAndMagnitude("METER", 1.0)
|
||||
serialiser.writeHeader()
|
||||
|
||||
print(f"Exporting {len(elements)} elements to {obj_path}")
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements)
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
for material in shape.geometry.materials:
|
||||
materials_collected.append(material.name)
|
||||
serialiser.write(shape)
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
serialiser.finalize()
|
||||
return materials_collected
|
||||
|
||||
def _sync_moved_object_placements(self):
|
||||
"""Sync Blender object positions to IFC ObjectPlacements for all moved objects.
|
||||
|
||||
This is critical for linked aggregate copies: their IFC ObjectPlacements
|
||||
are initially copies of the original's placement. After the user moves them
|
||||
in Blender, the IFC placements are stale until explicitly synced. Without
|
||||
this, the iterator with use-world-coords=True exports all copies at the
|
||||
original position.
|
||||
"""
|
||||
import bonsai.core.geometry as core_geometry
|
||||
|
||||
synced = 0
|
||||
for obj in bpy.data.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element is None:
|
||||
continue
|
||||
if not element.is_a("IfcProduct"):
|
||||
continue
|
||||
try:
|
||||
if tool.Ifc.is_moved(obj):
|
||||
core_geometry.edit_object_placement(
|
||||
ifc=tool.Ifc,
|
||||
geometry=tool.Geometry,
|
||||
surveyor=tool.Surveyor,
|
||||
obj=obj,
|
||||
apply_scale=False,
|
||||
)
|
||||
synced += 1
|
||||
except Exception as e:
|
||||
print(f"Could not sync placement for {obj.name}: {e}")
|
||||
if synced:
|
||||
print(f"Synced {synced} moved object placement(s) to IFC before export")
|
||||
|
||||
def _export_collection_instances_obj(self, context, output_dir):
|
||||
"""Export Blender collection instances as per-parent OBJ files.
|
||||
|
||||
Collection instances (empties with instance_type='COLLECTION') are purely
|
||||
Blender constructs — they don't exist in the IFC file. The ifcopenshell
|
||||
iterator ignores them entirely, so we must export their geometry via
|
||||
Blender's depsgraph which evaluates all instances with correct world transforms.
|
||||
|
||||
Each collection instance parent gets its own OBJ file because obj2mesh
|
||||
cannot handle all instances in a single file ("too many patch triangles").
|
||||
"""
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
|
||||
# Find which objects are collection instance parents (visible, not hidden)
|
||||
# Skip collections that belong to linked IFC models — those are already
|
||||
# exported by _export_linked_models() via the IFC serializer.
|
||||
instance_parents = set()
|
||||
for obj in bpy.data.objects:
|
||||
if obj.instance_type == "COLLECTION" and obj.instance_collection is not None:
|
||||
if not obj.visible_get():
|
||||
continue
|
||||
# Check if this collection contains linked IFC objects
|
||||
coll = obj.instance_collection
|
||||
is_linked_ifc = any("guids" in child for child in coll.all_objects if child.type == "MESH")
|
||||
if is_linked_ifc:
|
||||
print(f"Skipping collection instance '{obj.name}' (linked IFC model, exported via IFC serializer)")
|
||||
continue
|
||||
instance_parents.add(obj.name)
|
||||
|
||||
if not instance_parents:
|
||||
return
|
||||
|
||||
print(f"Found {len(instance_parents)} collection instance(s) to export")
|
||||
|
||||
# Export one OBJ per collection instance parent
|
||||
identity = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
|
||||
total_meshes = 0
|
||||
|
||||
for parent_name in sorted(instance_parents):
|
||||
obj_path = os.path.join(output_dir, f"instance_{parent_name}.obj")
|
||||
vert_offset = 0
|
||||
mesh_count = 0
|
||||
|
||||
with open(obj_path, "w") as f:
|
||||
f.write(f"# Collection instance geometry for {parent_name}\n")
|
||||
f.write("usemtl white\n\n")
|
||||
|
||||
for dep_inst in depsgraph.object_instances:
|
||||
if not dep_inst.is_instance:
|
||||
continue
|
||||
if dep_inst.parent is None:
|
||||
continue
|
||||
if dep_inst.parent.original.name != parent_name:
|
||||
continue
|
||||
|
||||
eval_obj = dep_inst.object
|
||||
if eval_obj.type != "MESH":
|
||||
continue
|
||||
|
||||
try:
|
||||
mesh = eval_obj.to_mesh()
|
||||
except RuntimeError:
|
||||
continue
|
||||
if mesh is None:
|
||||
continue
|
||||
|
||||
matrix = dep_inst.matrix_world
|
||||
f.write(f"g obj_{mesh_count}\n")
|
||||
|
||||
for v in mesh.vertices:
|
||||
co = matrix @ v.co
|
||||
f.write(f"v {co.x} {co.y} {co.z}\n")
|
||||
|
||||
mesh.calc_loop_triangles()
|
||||
for tri in mesh.loop_triangles:
|
||||
i0 = vert_offset + tri.vertices[0] + 1
|
||||
i1 = vert_offset + tri.vertices[1] + 1
|
||||
i2 = vert_offset + tri.vertices[2] + 1
|
||||
f.write(f"f {i0} {i1} {i2}\n")
|
||||
|
||||
vert_offset += len(mesh.vertices)
|
||||
mesh_count += 1
|
||||
eval_obj.to_mesh_clear()
|
||||
|
||||
if mesh_count == 0:
|
||||
try:
|
||||
os.remove(obj_path)
|
||||
except OSError:
|
||||
pass
|
||||
continue
|
||||
|
||||
# Identity matrix — geometry is already at world coordinates
|
||||
linked_model_exports.append((obj_path, "", identity))
|
||||
total_meshes += mesh_count
|
||||
print(f" {parent_name}: {mesh_count} meshes, {vert_offset} vertices")
|
||||
|
||||
if total_meshes > 0:
|
||||
print(f"Exported {total_meshes} instanced meshes across {len(linked_model_exports)} file(s)")
|
||||
|
||||
def _export_non_ifc_meshes(self, context, output_dir):
|
||||
"""Export visible Blender mesh objects that have no IFC entity.
|
||||
|
||||
These are plain Blender geometry (e.g. manually added planes, cubes)
|
||||
that don't exist in any IFC file. They are skipped by the IFC iterator
|
||||
and by the collection instance exporter, so we handle them separately.
|
||||
"""
|
||||
non_ifc_meshes = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
if not obj.visible_get():
|
||||
continue
|
||||
# Skip objects that have an IFC entity (handled by main/linked IFC export)
|
||||
if tool.Ifc.get_entity(obj) is not None:
|
||||
continue
|
||||
# Skip objects inside instanced collections (handled by collection instance export)
|
||||
if any(col.library for col in obj.users_collection):
|
||||
continue
|
||||
# Skip linked IFC element objects (have "guids" custom prop)
|
||||
if "guids" in obj:
|
||||
continue
|
||||
non_ifc_meshes.append(obj)
|
||||
|
||||
if not non_ifc_meshes:
|
||||
return
|
||||
|
||||
obj_path = os.path.join(output_dir, "blender_meshes.obj")
|
||||
vert_offset = 0
|
||||
mesh_count = 0
|
||||
identity = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
|
||||
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
|
||||
with open(obj_path, "w") as f:
|
||||
f.write("# Non-IFC Blender mesh geometry\n")
|
||||
f.write("usemtl white\n\n")
|
||||
|
||||
for obj in non_ifc_meshes:
|
||||
eval_obj = obj.evaluated_get(depsgraph)
|
||||
try:
|
||||
mesh = eval_obj.to_mesh()
|
||||
except RuntimeError:
|
||||
continue
|
||||
if mesh is None:
|
||||
continue
|
||||
|
||||
matrix = obj.matrix_world
|
||||
f.write(f"g {obj.name}\n")
|
||||
|
||||
for v in mesh.vertices:
|
||||
co = matrix @ v.co
|
||||
f.write(f"v {co.x} {co.y} {co.z}\n")
|
||||
|
||||
mesh.calc_loop_triangles()
|
||||
for tri in mesh.loop_triangles:
|
||||
i0 = vert_offset + tri.vertices[0] + 1
|
||||
i1 = vert_offset + tri.vertices[1] + 1
|
||||
i2 = vert_offset + tri.vertices[2] + 1
|
||||
f.write(f"f {i0} {i1} {i2}\n")
|
||||
|
||||
vert_offset += len(mesh.vertices)
|
||||
mesh_count += 1
|
||||
eval_obj.to_mesh_clear()
|
||||
|
||||
if mesh_count == 0:
|
||||
try:
|
||||
os.remove(obj_path)
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
|
||||
linked_model_exports.append((obj_path, "", identity))
|
||||
print(f"Exported {mesh_count} non-IFC Blender mesh(es) ({vert_offset} vertices)")
|
||||
|
||||
def execute(self, context):
|
||||
ifc_materials.clear()
|
||||
linked_model_exports.clear()
|
||||
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
output_dir = props.output_dir
|
||||
props.is_exporting = True
|
||||
|
||||
# Sync all moved Blender object positions to IFC before export.
|
||||
self._sync_moved_object_placements()
|
||||
|
||||
settings, serializer_settings = self._get_geom_settings()
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
# --- Export main model ---
|
||||
obj_file_path = os.path.join(output_dir, "model.obj")
|
||||
mtl_file_path = os.path.join(output_dir, "model.mtl")
|
||||
|
||||
visible_elements = self._get_exportable_elements(ifc_file, filter_visibility=True)
|
||||
mats = self._export_ifc_to_obj(
|
||||
ifc_file, obj_file_path, mtl_file_path, settings, serializer_settings, visible_elements
|
||||
)
|
||||
ifc_materials.extend(mats)
|
||||
|
||||
self.report({"INFO"}, f"Exported main model OBJ to: {obj_file_path}")
|
||||
|
||||
# --- Export linked models (external IFC files) ---
|
||||
self._export_linked_models(ifc_file, output_dir, settings, serializer_settings)
|
||||
|
||||
# --- Export collection instances (Blender-level linked copies) ---
|
||||
self._export_collection_instances_obj(context, output_dir)
|
||||
|
||||
# --- Export non-IFC Blender meshes (plain geometry with no IFC entity) ---
|
||||
self._export_non_ifc_meshes(context, output_dir)
|
||||
|
||||
props.is_exporting = False
|
||||
total_linked = len(linked_model_exports)
|
||||
if total_linked:
|
||||
self.report({"INFO"}, f"Also exported {total_linked} linked/instanced model(s)")
|
||||
return {"FINISHED"}
|
||||
|
||||
def _export_linked_models(self, main_ifc_file, output_dir, settings, serializer_settings):
|
||||
"""Detect and export all linked IFC models."""
|
||||
try:
|
||||
project_props = tool.Project.get_project_props()
|
||||
except Exception:
|
||||
print("Could not access project properties for linked models")
|
||||
return
|
||||
|
||||
for idx, link in enumerate(project_props.links):
|
||||
if not link.is_loaded:
|
||||
print(f"Skipping linked model '{link.name}' (not loaded)")
|
||||
continue
|
||||
|
||||
# Check if the link's empty handle is hidden in Blender
|
||||
try:
|
||||
link_empty = tool.Project.get_link_empty_handle(link)
|
||||
if link_empty is not None and not link_empty.visible_get():
|
||||
print(f"Skipping linked model '{link.name}' (hidden in viewport)")
|
||||
continue
|
||||
except Exception:
|
||||
pass # If we can't check visibility, export anyway
|
||||
|
||||
try:
|
||||
filepath = Path(tool.Ifc.resolve_uri(link.filepath))
|
||||
except Exception as e:
|
||||
print(f"Could not resolve path for linked model '{link.name}': {e}")
|
||||
continue
|
||||
|
||||
if not filepath.exists():
|
||||
print(f"Linked IFC file not found: {filepath}")
|
||||
continue
|
||||
|
||||
print(f"Exporting linked model {idx}: {filepath.name}")
|
||||
try:
|
||||
linked_ifc = ifcopenshell.open(str(filepath))
|
||||
except Exception as e:
|
||||
print(f" Failed to open linked IFC: {e}")
|
||||
continue
|
||||
|
||||
link_obj_path = os.path.join(output_dir, f"linked_{idx}.obj")
|
||||
link_mtl_path = os.path.join(output_dir, f"linked_{idx}.mtl")
|
||||
|
||||
# For linked models we don't filter by Blender visibility
|
||||
# (their objects are collection instances, not individually tracked)
|
||||
elements = self._get_exportable_elements(linked_ifc, filter_visibility=False)
|
||||
if not elements:
|
||||
print(f" No exportable elements found in linked model")
|
||||
continue
|
||||
|
||||
mats = self._export_ifc_to_obj(
|
||||
linked_ifc, link_obj_path, link_mtl_path, settings, serializer_settings, elements
|
||||
)
|
||||
ifc_materials.extend(mats)
|
||||
|
||||
# Get the link transformation matrix
|
||||
try:
|
||||
link_matrix = tool.Project.calculate_link_matrix(link)
|
||||
matrix_list = [list(row) for row in link_matrix]
|
||||
except Exception as e:
|
||||
print(f" Could not calculate link matrix: {e}, using identity")
|
||||
matrix_list = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
|
||||
|
||||
linked_model_exports.append((link_obj_path, link_mtl_path, matrix_list))
|
||||
print(f" Exported {len(elements)} elements from linked model '{link.name}'")
|
||||
|
||||
|
||||
class CleanupRadianceFiles(bpy.types.Operator):
|
||||
"""Delete all generated Radiance files from the output directory"""
|
||||
|
||||
bl_idname = "radiance.cleanup_files"
|
||||
bl_label = "Cleanup Radiance Files"
|
||||
bl_description = "Remove all generated files (OBJ, MTL, RTM, RAD, HDR, TIFF, DAT) from the output directory"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
if not props.output_dir:
|
||||
cls.poll_message_set("Output directory is not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
import bonsai.bim.module.light.shared as shared
|
||||
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
output_dir = props.output_dir
|
||||
|
||||
if not os.path.isdir(output_dir):
|
||||
self.report({"WARNING"}, f"Output directory does not exist: {output_dir}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
cleanup_patterns = (
|
||||
"model.obj",
|
||||
"model.mtl",
|
||||
"model.rtm",
|
||||
"sky.rad",
|
||||
"materials.rad",
|
||||
"scene.rad",
|
||||
"ascene.oct",
|
||||
"mascene.oct",
|
||||
"ascene.amb",
|
||||
)
|
||||
cleanup_extensions = (".hdr", ".tiff", ".rad", ".dat")
|
||||
cleanup_prefixes = ("instance_", "linked_", "blender_meshes")
|
||||
|
||||
removed = 0
|
||||
for filename in os.listdir(output_dir):
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
is_generated = (
|
||||
filename in cleanup_patterns
|
||||
or os.path.splitext(filename)[1].lower() in cleanup_extensions
|
||||
or any(filename.startswith(p) for p in cleanup_prefixes)
|
||||
)
|
||||
if is_generated:
|
||||
try:
|
||||
os.remove(filepath)
|
||||
removed += 1
|
||||
except OSError as e:
|
||||
print(f"Failed to remove {filepath}: {e}")
|
||||
|
||||
# Reset the global scene reference
|
||||
shared.scene = None
|
||||
|
||||
self.report({"INFO"}, f"Cleaned up {removed} generated files from {output_dir}")
|
||||
return {"FINISHED"}
|
||||
@@ -1,80 +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/>.
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class AddIESLight(bpy.types.Operator, ImportHelper):
|
||||
"""Upload and add an IES light fixture to the scene"""
|
||||
|
||||
bl_idname = "radiance.add_ies_light"
|
||||
bl_label = "Add IES Light"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
filename_ext = ".ies"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ies;*.IES", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
|
||||
# Create new IES light entry
|
||||
ies_light = props.ies_lights.add()
|
||||
# Store as relative path if the blend file is saved
|
||||
if bpy.data.filepath:
|
||||
ies_light.ies_file_path = bpy.path.relpath(self.filepath)
|
||||
else:
|
||||
ies_light.ies_file_path = self.filepath
|
||||
ies_light.rotation_z = 0.0
|
||||
ies_light.is_enabled = True
|
||||
|
||||
# Set as active
|
||||
props.active_ies_light_index = len(props.ies_lights) - 1
|
||||
|
||||
self.report({"INFO"}, f"Added IES light: {Path(self.filepath).name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveIESLight(bpy.types.Operator):
|
||||
"""Remove an IES light fixture mapping"""
|
||||
|
||||
bl_idname = "radiance.remove_ies_light"
|
||||
bl_label = "Remove IES Light"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
|
||||
if 0 <= self.index < len(props.ies_lights):
|
||||
props.ies_lights.remove(self.index)
|
||||
|
||||
# Adjust active index if needed
|
||||
if props.active_ies_light_index >= len(props.ies_lights):
|
||||
props.active_ies_light_index = len(props.ies_lights) - 1
|
||||
|
||||
self.report({"INFO"}, "IES light removed")
|
||||
return {"FINISHED"}
|
||||
|
||||
self.report({"WARNING"}, "Invalid IES light index")
|
||||
return {"CANCELLED"}
|
||||
@@ -18,18 +18,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.light.prop import (
|
||||
IESLight,
|
||||
RadianceExporterProperties,
|
||||
RadianceMaterial,
|
||||
)
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||
spectraldb = json.load(f)
|
||||
|
||||
|
||||
class MATERIAL_UL_radiance_materials(bpy.types.UIList):
|
||||
def draw_item(
|
||||
@@ -61,41 +64,3 @@ class MATERIAL_UL_radiance_materials(bpy.types.UIList):
|
||||
op.material_index = index
|
||||
else:
|
||||
row.label(text="Not Mapped (White)")
|
||||
|
||||
|
||||
class MATERIAL_UL_ies_lights(bpy.types.UIList):
|
||||
"""UIList for displaying IES light fixtures."""
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: RadianceExporterProperties,
|
||||
item: IESLight,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
index,
|
||||
) -> None:
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
row = layout.row(align=True)
|
||||
|
||||
# Enable/disable checkbox (visible toggle)
|
||||
row.prop(item, "is_enabled", text="", emboss=True)
|
||||
|
||||
# IES file name
|
||||
if item.ies_file_path:
|
||||
filename = Path(item.ies_file_path).name
|
||||
row.label(text=filename, icon="FILE")
|
||||
else:
|
||||
row.label(text="(No file selected)", icon="ERROR")
|
||||
|
||||
# Target: collection or single object
|
||||
if item.use_collection:
|
||||
row.prop(item, "target_collection", text="", icon="OUTLINER_COLLECTION", emboss=False)
|
||||
else:
|
||||
row.prop(item, "target_object", text="", emboss=False)
|
||||
|
||||
# Remove button (X icon - negative action)
|
||||
op = row.operator("radiance.remove_ies_light", text="", icon="X")
|
||||
op.index = index
|
||||
|
||||
@@ -1,136 +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/>.
|
||||
|
||||
import json
|
||||
import webbrowser
|
||||
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class RefreshIFCMaterials(bpy.types.Operator):
|
||||
bl_idname = "bim.refresh_ifc_materials"
|
||||
bl_label = "Refresh IFC Materials"
|
||||
bl_description = "Refresh the list of IFC materials for mapping"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC file loaded in Bonsai.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
props.materials.clear()
|
||||
|
||||
for style in ifc_file.by_type("IfcSurfaceStyle"):
|
||||
for render_item in style.Styles:
|
||||
if render_item.is_a("IfcSurfaceStyleRendering"):
|
||||
style_id = f"IfcSurfaceStyleRendering-{render_item.id()}"
|
||||
style_name = style.Name or f"Unnamed Style {render_item.id()}"
|
||||
|
||||
# Extract color
|
||||
color = (1.0, 1.0, 1.0) # Default white
|
||||
if render_item.SurfaceColour:
|
||||
color = (
|
||||
render_item.SurfaceColour.Red,
|
||||
render_item.SurfaceColour.Green,
|
||||
render_item.SurfaceColour.Blue,
|
||||
)
|
||||
|
||||
material = props.add_material_mapping(style_id, style_name)
|
||||
material.color = color
|
||||
|
||||
material.category = ""
|
||||
material.subcategory = ""
|
||||
material.is_mapped = False
|
||||
|
||||
props.active_material_index = 0 if props.materials else -1
|
||||
|
||||
self.report({"INFO"}, f"Refreshed {len(props.materials)} IFC materials")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UnmapMaterial(bpy.types.Operator):
|
||||
bl_idname = "bim.unmap_material"
|
||||
bl_label = "Unmap Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
material_index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
material = props.materials[self.material_index]
|
||||
props.unmap_material(material.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "radiance.export_material_mappings"
|
||||
bl_label = "Export Material Mappings"
|
||||
bl_description = "Export material mappings to a JSON file"
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
mappings = {}
|
||||
|
||||
for material in props.materials:
|
||||
if material.is_mapped:
|
||||
mappings[material.style_id] = {
|
||||
"name": material.name,
|
||||
"category": material.category,
|
||||
"subcategory": material.subcategory,
|
||||
}
|
||||
|
||||
with open(self.filepath, "w") as f:
|
||||
json.dump(mappings, f, indent=4)
|
||||
|
||||
self.report({"INFO"}, f"Material mappings exported to {self.filepath}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RADIANCE_OT_import_material_mappings(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "radiance.import_material_mappings"
|
||||
bl_label = "Import Material Mappings"
|
||||
bl_description = "Import material mappings from a JSON file"
|
||||
|
||||
filename_ext = ".json"
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
props.import_mappings(self.filepath)
|
||||
self.report({"INFO"}, f"Material mappings imported from {self.filepath}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RADIANCE_OT_open_spectraldb(bpy.types.Operator):
|
||||
bl_idname = "radiance.open_spectraldb"
|
||||
bl_label = "Open SpectralDB"
|
||||
bl_description = "Open the SpectralDB website for reference"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://spectraldb.com")
|
||||
return {"FINISHED"}
|
||||
@@ -16,44 +16,716 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Thin re-export module for backward compatibility.
|
||||
import json
|
||||
import math
|
||||
import multiprocessing
|
||||
import os
|
||||
import time
|
||||
import webbrowser
|
||||
from datetime import datetime
|
||||
from math import radians
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
All operator classes are defined in their respective submodules:
|
||||
- export.py: ExportOBJ, CleanupRadianceFiles
|
||||
- prepare.py: PrepareRadianceScene
|
||||
- render.py: RadianceRender, FalseColorRadiance, RADIANCE_OT_select_camera
|
||||
- solar.py: ImportTrueNorth, ImportLatLong, MoveSunPathTo3DCursor,
|
||||
ViewFromSun, LightPickCoordinates, LightSetTimeToNow
|
||||
- material.py: RefreshIFCMaterials, UnmapMaterial,
|
||||
RADIANCE_OT_export_material_mappings,
|
||||
RADIANCE_OT_import_material_mappings,
|
||||
RADIANCE_OT_open_spectraldb
|
||||
- ies.py: AddIESLight, RemoveIESLight
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.geolocation
|
||||
import pyradiance as pr
|
||||
import requests
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from mathutils import Vector
|
||||
|
||||
EnumPropertySearch and SetEnumProperty are provided by the global
|
||||
bonsai.bim.operator module (bl_idname "bim.enum_property_search").
|
||||
"""
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.light.data import SolarData
|
||||
|
||||
from bonsai.bim.module.light.export import CleanupRadianceFiles, ExportOBJ # noqa: F401
|
||||
from bonsai.bim.module.light.ies import AddIESLight, RemoveIESLight # noqa: F401
|
||||
from bonsai.bim.module.light.material import ( # noqa: F401
|
||||
RADIANCE_OT_export_material_mappings,
|
||||
RADIANCE_OT_import_material_mappings,
|
||||
RADIANCE_OT_open_spectraldb,
|
||||
RefreshIFCMaterials,
|
||||
UnmapMaterial,
|
||||
)
|
||||
from bonsai.bim.module.light.prepare import PrepareRadianceScene # noqa: F401
|
||||
from bonsai.bim.module.light.render import ( # noqa: F401
|
||||
FalseColorRadiance,
|
||||
RADIANCE_OT_select_camera,
|
||||
RadianceRender,
|
||||
)
|
||||
from bonsai.bim.module.light.solar import ( # noqa: F401
|
||||
ImportLatLong,
|
||||
ImportTrueNorth,
|
||||
LightPickCoordinates,
|
||||
LightSetTimeToNow,
|
||||
MoveSunPathTo3DCursor,
|
||||
ViewFromSun,
|
||||
)
|
||||
ifc_materials = []
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||
spectraldb = json.load(f)
|
||||
|
||||
|
||||
class ExportOBJ(bpy.types.Operator):
|
||||
"""Exports the IFC File to OBJ"""
|
||||
|
||||
bl_idname = "export_scene.radiance"
|
||||
bl_label = "Export"
|
||||
bl_description = "Export the IFC to OBJ"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
if not props.should_load_from_memory and not props.ifc_file:
|
||||
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
# Get the output directory
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
should_load_from_memory = props.should_load_from_memory
|
||||
output_dir = props.output_dir
|
||||
|
||||
props.is_exporting = True
|
||||
|
||||
# Conversion from IFC to OBJ
|
||||
# Settings for obj
|
||||
settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS)
|
||||
settings.set("apply-default-materials", True)
|
||||
serializer_settings.set("use-element-guids", True)
|
||||
settings.set("use-world-coords", True)
|
||||
|
||||
ifc_file: ifcopenshell.file
|
||||
if should_load_from_memory:
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
else:
|
||||
ifc_file_path = props.ifc_file
|
||||
ifc_file = ifcopenshell.open(ifc_file_path)
|
||||
|
||||
obj_file_path = os.path.join(output_dir, "model.obj")
|
||||
mtl_file_path = os.path.join(output_dir, "model.mtl")
|
||||
|
||||
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings)
|
||||
serialiser.setFile(ifc_file)
|
||||
serialiser.setUnitNameAndMagnitude("METER", 1.0)
|
||||
serialiser.writeHeader()
|
||||
|
||||
if ifc_file.schema in ("IFC2X3", "IFC4"):
|
||||
elements = ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcProxy")
|
||||
else:
|
||||
elements = ifc_file.by_type("IfcElement")
|
||||
|
||||
elements += ifc_file.by_type("IfcSite")
|
||||
elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
|
||||
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements)
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
materials = shape.geometry.materials
|
||||
|
||||
for material in materials:
|
||||
ifc_materials.append(material.name)
|
||||
|
||||
serialiser.write(shape)
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
serialiser.finalize()
|
||||
props.is_exporting = False
|
||||
|
||||
self.report({"INFO"}, "Exported OBJ file to: {}".format(obj_file_path))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RadianceRender(bpy.types.Operator):
|
||||
"""Radiance Rendering"""
|
||||
|
||||
bl_idname = "render_scene.radiance"
|
||||
bl_label = "Render"
|
||||
bl_description = "Renders the scene using Radiance"
|
||||
|
||||
def execute(self, context):
|
||||
print("Starting Radiance rendering process...")
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y
|
||||
|
||||
assert context.scene
|
||||
context.scene.render.resolution_x = resolution_x
|
||||
context.scene.render.resolution_y = resolution_y
|
||||
|
||||
aspect_ratio = resolution_x / resolution_y
|
||||
|
||||
quality = props.radiance_quality.upper()
|
||||
detail = props.radiance_detail.upper()
|
||||
variability = props.radiance_variability.upper()
|
||||
output_dir = props.output_dir
|
||||
output_file_name = props.output_file_name
|
||||
output_file_format = props.output_file_format
|
||||
use_hdr = props.use_hdr
|
||||
choose_hdr_image = props.choose_hdr_image
|
||||
|
||||
print(f"Resolution: {resolution_x}x{resolution_y}")
|
||||
print(f"Quality: {quality}, Detail: {detail}, Variability: {variability}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
|
||||
if use_hdr:
|
||||
hdr_image = "noon_grass_2k.hdr"
|
||||
hdr_mask = "noon_grass_2k_mask.hdr"
|
||||
sky_map_cal = "skymap.cal"
|
||||
hdr_image_path = os.path.join(os.path.dirname(__file__), "HDRs", hdr_image)
|
||||
hdr_mask_path = os.path.join(os.path.dirname(__file__), "HDRs", hdr_mask)
|
||||
sky_map_cal_path = os.path.join(os.path.dirname(__file__), "HDRs", sky_map_cal)
|
||||
|
||||
obj_file_path = os.path.join(output_dir, "model.obj")
|
||||
|
||||
sun_props = tool.Blender.get_solar_props()
|
||||
sun_pos_props = tool.Blender.get_sun_props()
|
||||
assert sun_pos_props
|
||||
sky_file_path = os.path.join(output_dir, "sky.rad")
|
||||
# latitude = sun_props.latitude
|
||||
# longitude = sun_props.longitude
|
||||
# month = sun_props.month
|
||||
# day = sun_props.day
|
||||
# hour = sun_props.hour
|
||||
# minute = sun_props.minute
|
||||
|
||||
# print("Sun Properties:")
|
||||
# print("Latitude: ", latitude)
|
||||
# print("Longitude: ", longitude)
|
||||
# print("Timezone: ", timezone)
|
||||
# print("Month: ", month)
|
||||
# print("Day: ", day)
|
||||
# print("Hour: ", hour)
|
||||
# print("Minute: ", minute)
|
||||
|
||||
print("Setting up camera...")
|
||||
if props.use_active_camera:
|
||||
camera = context.scene.camera
|
||||
else:
|
||||
camera = props.selected_camera
|
||||
|
||||
if camera is None:
|
||||
self.report({"ERROR"}, "No active camera found in the scene. Please add a camera and set it as active.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Get camera position and direction
|
||||
camera_position, camera_direction = self.get_camera_data(camera)
|
||||
|
||||
print(f"Camera position: {camera_position}")
|
||||
print(f"Camera direction: {camera_direction}")
|
||||
|
||||
# sun_position = tool.Blender.get_addon("sun_position")
|
||||
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
|
||||
# sun_pos_props.time,
|
||||
# sun_pos_props.latitude,
|
||||
# sun_pos_props.longitude,
|
||||
# -sun_pos_props.UTC_zone,
|
||||
# sun_pos_props.month,
|
||||
# sun_pos_props.day,
|
||||
# sun_pos_props.year,
|
||||
# )
|
||||
|
||||
dt = datetime(sun_pos_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute)
|
||||
|
||||
sky_description = pr.gensky(
|
||||
dt=dt,
|
||||
# azimuth=64.1,
|
||||
# altitude=-26.6,
|
||||
latitude=sun_props.latitude,
|
||||
longitude=sun_props.longitude,
|
||||
year=sun_pos_props.year,
|
||||
timezone=-int(sun_props.UTC_zone),
|
||||
# sunny_with_sun=False,
|
||||
# sunny_without_sun=False,
|
||||
# cloudy=False,
|
||||
# ground_reflectance=0.2,
|
||||
# turbidity=3.0,
|
||||
)
|
||||
|
||||
sky_description_str = sky_description.decode("utf-8")
|
||||
|
||||
# Write all this to file
|
||||
# skyfunc glow sky_glow
|
||||
# 0
|
||||
# 0
|
||||
# 4 .9 .9 1.15 0
|
||||
|
||||
# sky_glow source sky
|
||||
# 0
|
||||
# 0
|
||||
# 4 0 0 1 180
|
||||
|
||||
# skyfunc glow ground_glow
|
||||
# 0
|
||||
# 0
|
||||
# 4 1.4 .9 .6 0
|
||||
|
||||
# ground_glow source ground
|
||||
# 0
|
||||
# 0
|
||||
# 4 0 0 -1 180
|
||||
|
||||
if use_hdr and choose_hdr_image == "Noon":
|
||||
|
||||
with open(sky_file_path, "w") as f:
|
||||
f.write(sky_description_str)
|
||||
f.write("\n")
|
||||
# f.write("skyfunc glow sky_glow\n0\n0\n4 .9 .9 1.15 0\n")
|
||||
# f.write("sky_glow source sky\n0\n0\n4 0 0 1 180\n")
|
||||
# f.write("skyfunc glow ground_glow\n0\n0\n4 1.4 .9 .6 0\n")
|
||||
# f.write("ground_glow source ground\n0\n0\n4 0 0 -1 180\n")
|
||||
|
||||
f.write(
|
||||
'''void colorpict env_map
|
||||
7 red green blue "'''
|
||||
+ hdr_image_path
|
||||
+ '''" "'''
|
||||
+ sky_map_cal_path
|
||||
+ '''" map_u map_v
|
||||
0
|
||||
1 0.5
|
||||
|
||||
# This is a multiplier to colour balance the env map
|
||||
# In this case, it provides a rough ground luminance from 3k-5k
|
||||
env_map colorfunc env_colour
|
||||
4 100 100 100 .
|
||||
0
|
||||
0
|
||||
|
||||
# .37 .57 1.5 is measured from a HDRI image
|
||||
# It is multiplied by a factor such that grey(r,g,b) = 1
|
||||
skyfunc colorfunc sky_colour
|
||||
4 .64 .99 2.6 .
|
||||
0
|
||||
0
|
||||
|
||||
void mixpict composite
|
||||
7 env_colour sky_colour grey "'''
|
||||
+ hdr_mask_path
|
||||
+ '''" "'''
|
||||
+ sky_map_cal_path
|
||||
+ """" map_u map_v
|
||||
0
|
||||
2 0.5 1
|
||||
|
||||
composite glow env_map_glow
|
||||
0
|
||||
0
|
||||
4 1 1 1 0
|
||||
|
||||
env_map_glow source sky
|
||||
0
|
||||
0
|
||||
4 0 0 1 180
|
||||
|
||||
env_colour glow ground_glow
|
||||
0
|
||||
0
|
||||
4 1 1 1 0
|
||||
|
||||
ground_glow source ground
|
||||
0
|
||||
0
|
||||
4 0 0 -1 180"""
|
||||
)
|
||||
|
||||
elif not use_hdr:
|
||||
with open(sky_file_path, "w") as f:
|
||||
f.write(sky_description_str)
|
||||
f.write("\n")
|
||||
# f.write("skyfunc glow sky_glow\n0\n0\n4 .9 .9 1.15 0\n")
|
||||
# f.write("sky_glow source sky\n0\n0\n4 0 0 1 180\n")
|
||||
# f.write("skyfunc glow ground_glow\n0\n0\n4 1.4 .9 .6 0\n")
|
||||
# f.write("ground_glow source ground\n0\n0\n4 0 0 -1 180\n")
|
||||
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
|
||||
data = props.get_mappings_dict()
|
||||
|
||||
materials_file = os.path.join(output_dir, "materials.rad")
|
||||
written_materials = set()
|
||||
|
||||
all_materials = set(ifc_materials)
|
||||
|
||||
with open(materials_file, "w") as file:
|
||||
# Write default materials
|
||||
default_materials = [
|
||||
"void plastic white\n0\n0\n5 0.8 0.8 0.8 0 0\n",
|
||||
# "void plastic blue_plastic\n0\n0\n5 0.1 0.2 0.8 0.05 0.1\n",
|
||||
# "void plastic red_plastic\n0\n0\n5 0.8 0.1 0.2 0.05 0.1\n",
|
||||
# "void metal silver_metal\n0\n0\n5 0.8 0.8 0.8 0.9 0.1\n",
|
||||
# "void glass clear_glass\n0\n0\n3 0.96 0.96 0.96\n",
|
||||
# "void light white_light\n0\n0\n3 1.0 1.0 1.0\n",
|
||||
# "void trans olive_trans\n0\n0\n7 0.6 0.7 0.4 0.05 0.05 0.7 0.2\n",
|
||||
]
|
||||
for material in default_materials:
|
||||
file.write(material)
|
||||
written_materials.add(material.split()[2]) # Add material name to written set
|
||||
|
||||
for style_id in all_materials:
|
||||
material = next((m for m in props.materials if m.style_id == style_id), None)
|
||||
if material and material.is_mapped:
|
||||
category, subcategory = material.category, material.subcategory
|
||||
if category in spectraldb and subcategory in spectraldb[category]:
|
||||
material_def = spectraldb[category][subcategory]
|
||||
material_name = material_def.split()[2]
|
||||
if material_name not in written_materials:
|
||||
file.write(material_def + "\n")
|
||||
written_materials.add(material_name)
|
||||
file.write(f"inherit alias {style_id} {material_name}\n")
|
||||
else:
|
||||
file.write(f"inherit alias {style_id} white\n")
|
||||
else:
|
||||
# If the material is not mapped, alias it to white
|
||||
file.write(f"inherit alias {style_id} white\n")
|
||||
|
||||
self.report({"INFO"}, f"Exported Materials Rad file to: {materials_file}")
|
||||
|
||||
# Run obj2mesh
|
||||
rtm_file_path = os.path.join(output_dir, "model.rtm")
|
||||
mesh_file_path = save_obj2mesh_output(obj_file_path, rtm_file_path, matfiles=[materials_file])
|
||||
# subprocess.run(["obj2mesh", "-a", materials_file, obj_file_path, rtm_file_path])
|
||||
self.report({"INFO"}, "obj2mesh output: {}".format(mesh_file_path))
|
||||
scene_file = os.path.join(output_dir, "scene.rad")
|
||||
with open(scene_file, "w") as file:
|
||||
file.write('void mesh model\n1 "' + rtm_file_path + '"\n0\n0\n')
|
||||
|
||||
self.report({"INFO"}, "Exported Scene file to: {}".format(scene_file))
|
||||
|
||||
print("Setting up Radiance scene...")
|
||||
scene = pr.Scene("ascene")
|
||||
|
||||
material_path = os.path.join(output_dir, "materials.rad")
|
||||
scene_path = os.path.join(output_dir, "scene.rad")
|
||||
|
||||
scene.add_material(material_path)
|
||||
scene.add_surface(scene_path)
|
||||
scene.add_source(sky_file_path)
|
||||
print("Setting up view...")
|
||||
assert isinstance(camera.data, bpy.types.Camera)
|
||||
if camera.data.type == "PERSP":
|
||||
# Perspective camera
|
||||
camera_fov = camera.data.angle
|
||||
# Calculate vertical FOV based on the desired aspect ratio
|
||||
vertical_fov = 2 * math.atan(math.tan(camera_fov / 2) / aspect_ratio)
|
||||
|
||||
aview = pr.View(
|
||||
vtype="v", # Perspective view
|
||||
position=camera_position,
|
||||
direction=camera_direction,
|
||||
vup=(0, 0, 1), # Assuming Z is up
|
||||
horiz=math.degrees(camera_fov),
|
||||
vert=math.degrees(vertical_fov),
|
||||
)
|
||||
else: # 'ORTHO'
|
||||
# Orthographic camera
|
||||
# Calculate the view size based on the camera's orthographic scale
|
||||
ortho_scale = camera.data.ortho_scale
|
||||
view_width = ortho_scale
|
||||
view_height = ortho_scale / aspect_ratio
|
||||
|
||||
aview = pr.View(
|
||||
vtype="l", # Parallel projection (orthographic)
|
||||
position=camera_position,
|
||||
direction=camera_direction,
|
||||
vup=(0, 0, 1), # Assuming Z is up
|
||||
horiz=view_width,
|
||||
vert=view_height,
|
||||
)
|
||||
scene.add_view(aview)
|
||||
print("Starting render...")
|
||||
start_time = time.time()
|
||||
image = pr.render(
|
||||
scene,
|
||||
ambbounce=1,
|
||||
resolution=(resolution_x, resolution_y),
|
||||
quality=quality,
|
||||
detail=detail,
|
||||
variability=variability,
|
||||
nproc=multiprocessing.cpu_count(),
|
||||
)
|
||||
end_time = time.time()
|
||||
print(f"Render completed in {end_time - start_time:.2f} seconds")
|
||||
|
||||
output_hdr_path = os.path.join(output_dir, f"{output_file_name}.{output_file_format.lower()}")
|
||||
print(f"Saving HDR output to: {output_hdr_path}")
|
||||
if output_file_format == "HDR":
|
||||
with open(output_hdr_path, "wb") as wtr:
|
||||
wtr.write(image)
|
||||
else:
|
||||
pass
|
||||
print("Applying tone mapping...")
|
||||
pcond_image = pr.pcond(hdr=output_hdr_path, human=True)
|
||||
|
||||
tiff_path = os.path.join(output_dir, f"{output_file_name}.tiff")
|
||||
print(f"Saving TIFF output to: {tiff_path}")
|
||||
pr.ra_tiff(inp=pcond_image, out=tiff_path, lzw=True)
|
||||
print("Radiance rendering process completed successfully.")
|
||||
self.report({"INFO"}, "Radiance rendering completed. Output: {}".format(tiff_path))
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_active_camera(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
if props.use_active_camera:
|
||||
return context.scene.camera
|
||||
else:
|
||||
return props.selected_camera
|
||||
|
||||
def get_camera_data(self, camera):
|
||||
# Get camera position
|
||||
position = camera.matrix_world.to_translation()
|
||||
|
||||
# Get camera direction
|
||||
direction = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
direction.normalize()
|
||||
|
||||
return (position.x, position.y, position.z), (direction.x, direction.y, direction.z)
|
||||
|
||||
def getResolution(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
resolution_x = props.radiance_resolution_x
|
||||
resolution_y = props.radiance_resolution_y
|
||||
return resolution_x, resolution_y
|
||||
|
||||
|
||||
def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs):
|
||||
output_bytes = pr.obj2mesh(inp, **kwargs)
|
||||
|
||||
with open(output_file, "wb") as f:
|
||||
f.write(output_bytes)
|
||||
return output_file
|
||||
|
||||
|
||||
class ImportTrueNorth(bpy.types.Operator):
|
||||
bl_idname = "bim.import_true_north"
|
||||
bl_label = "Import True North"
|
||||
bl_description = "Imports the True North from your IFC geometric context"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
if not SolarData.is_loaded:
|
||||
SolarData.load()
|
||||
return SolarData.data["true_north"] is not None
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
if not context.TrueNorth:
|
||||
continue
|
||||
value = context.TrueNorth.DirectionRatios
|
||||
props.true_north = radians(ifcopenshell.util.geolocation.yaxis2angle(*value[:2]))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImportLatLong(bpy.types.Operator):
|
||||
bl_idname = "bim.import_lat_long"
|
||||
bl_label = "Import Latitude / Longitude"
|
||||
bl_description = "Imports the latitude / longitude from an IfcSite"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
site = tool.Ifc.get().by_id(int(props.sites))
|
||||
if site.RefLatitude and site.RefLongitude:
|
||||
props.latitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLatitude)
|
||||
props.longitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLongitude)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class MoveSunPathTo3DCursor(bpy.types.Operator):
|
||||
bl_idname = "bim.move_sun_path_to_3d_cursor"
|
||||
bl_label = "Move Sun Path To 3D Cursor"
|
||||
bl_description = "Shifts the visualisation of the Sun Path to the 3D cursor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
assert context.scene
|
||||
props.sun_path_origin = context.scene.cursor.location
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ViewFromSun(bpy.types.Operator):
|
||||
bl_idname = "bim.view_from_sun"
|
||||
bl_label = "View From Sun"
|
||||
bl_description = "Views your model as if you were looking from the perspective of the sun"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
if not (camera := bpy.data.objects.get("SunPathCamera")):
|
||||
camera = bpy.data.objects.new("SunPathCamera", bpy.data.cameras.new("SunPathCamera"))
|
||||
assert isinstance(camera.data, bpy.types.Camera)
|
||||
assert context.scene
|
||||
camera.data.type = "ORTHO"
|
||||
camera.data.ortho_scale = 100 # The default of 6m is too small
|
||||
context.scene.collection.objects.link(camera)
|
||||
tool.Blender.activate_camera(camera)
|
||||
props = tool.Blender.get_solar_props()
|
||||
props.hour = props.hour # Just to refresh camera position
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LightPickCoordinates(bpy.types.Operator):
|
||||
bl_idname = "bim.light_pick_coordinates"
|
||||
bl_label = "Pick Coordinates"
|
||||
bl_description = (
|
||||
"Open web browser with Google Maps to pick coordinates (Right Mouse Click in maps to copy selected location).\n\n"
|
||||
"ALT+Click to insert current location based on the current IP-address (using ip-api.com)."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
use_current_location: bool
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.alt:
|
||||
self.use_current_location = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
if not self.use_current_location:
|
||||
zoom = 13.5
|
||||
url = f"https://www.google.com/maps/@{props.latitude},{props.longitude},{zoom}z"
|
||||
webbrowser.open(url)
|
||||
return {"FINISHED"}
|
||||
|
||||
response = requests.get("http://ip-api.com/json/")
|
||||
data = response.json()
|
||||
props.latitude = data["lat"]
|
||||
props.longitude = data["lon"]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LightSetTimeToNow(bpy.types.Operator):
|
||||
bl_idname = "bim.light_set_time_to_now"
|
||||
bl_label = "Now"
|
||||
bl_description = "Set time to current local time."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
props.set_from_datetime(datetime.now())
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RefreshIFCMaterials(bpy.types.Operator):
|
||||
bl_idname = "bim.refresh_ifc_materials"
|
||||
bl_label = "Refresh IFC Materials"
|
||||
bl_description = "Refresh the list of IFC materials for mapping"
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
ifc_file: ifcopenshell.file
|
||||
ifc_file = tool.Ifc.get() if props.should_load_from_memory else ifcopenshell.open(props.ifc_file)
|
||||
|
||||
props.materials.clear()
|
||||
|
||||
for style in ifc_file.by_type("IfcSurfaceStyle"):
|
||||
for render_item in style.Styles:
|
||||
if render_item.is_a("IfcSurfaceStyleRendering"):
|
||||
style_id = f"IfcSurfaceStyleRendering-{render_item.id()}"
|
||||
style_name = style.Name or f"Unnamed Style {render_item.id()}"
|
||||
|
||||
# Extract color and transparency
|
||||
color = (1.0, 1.0, 1.0) # Default white
|
||||
transparency = 0.0 # Default opaque
|
||||
if render_item.SurfaceColour:
|
||||
color = (
|
||||
render_item.SurfaceColour.Red,
|
||||
render_item.SurfaceColour.Green,
|
||||
render_item.SurfaceColour.Blue,
|
||||
)
|
||||
if hasattr(render_item, "Transparency") and render_item.Transparency is not None:
|
||||
transparency = render_item.Transparency
|
||||
|
||||
# Add material with color
|
||||
material = props.add_material_mapping(style_id, style_name)
|
||||
material.color = color
|
||||
|
||||
# If transparency is high, consider it as glass
|
||||
if transparency > 0.5:
|
||||
material.category = "Glass"
|
||||
material.subcategory = "Clear Glass"
|
||||
material.is_mapped = True
|
||||
|
||||
props.active_material_index = 0 if props.materials else -1
|
||||
|
||||
self.report({"INFO"}, f"Refreshed {len(props.materials)} IFC materials")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UnmapMaterial(bpy.types.Operator):
|
||||
bl_idname = "bim.unmap_material"
|
||||
bl_label = "Unmap Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
material_index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
material = props.materials[self.material_index]
|
||||
props.unmap_material(material.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RADIANCE_OT_select_camera(bpy.types.Operator):
|
||||
bl_idname = "radiance.select_camera"
|
||||
bl_label = "Select Camera"
|
||||
bl_description = "Select a camera from the viewport"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object is not None and context.object.type == "CAMERA"
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
props.selected_camera = context.object
|
||||
props.use_active_camera = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "radiance.export_material_mappings"
|
||||
bl_label = "Export Material Mappings"
|
||||
bl_description = "Export material mappings to a JSON file"
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
mappings = {}
|
||||
|
||||
for material in props.materials:
|
||||
if material.is_mapped:
|
||||
mappings[material.style_id] = {
|
||||
"name": material.name,
|
||||
"category": material.category,
|
||||
"subcategory": material.subcategory,
|
||||
}
|
||||
|
||||
with open(self.filepath, "w") as f:
|
||||
json.dump(mappings, f, indent=4)
|
||||
|
||||
self.report({"INFO"}, f"Material mappings exported to {self.filepath}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RADIANCE_OT_import_material_mappings(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "radiance.import_material_mappings"
|
||||
bl_label = "Import Material Mappings"
|
||||
bl_description = "Import material mappings from a JSON file"
|
||||
|
||||
filename_ext = ".json"
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
props.import_mappings(self.filepath)
|
||||
self.report({"INFO"}, f"Material mappings imported from {self.filepath}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RADIANCE_OT_open_spectraldb(bpy.types.Operator):
|
||||
bl_idname = "radiance.open_spectraldb"
|
||||
bl_label = "Open SpectralDB"
|
||||
bl_description = "Open the SpectralDB website for reference"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://spectraldb.com")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1,741 +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/>.
|
||||
|
||||
import math
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import bpy
|
||||
import pyradiance as pr
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.light.prop import spectraldb
|
||||
from bonsai.bim.module.light.shared import ifc_materials, linked_model_exports
|
||||
|
||||
|
||||
def _matrix_to_xform_args(matrix: list[list[float]]) -> str:
|
||||
"""Decompose a 4x4 matrix into Radiance xform arguments.
|
||||
|
||||
Decomposes into Z/Y/X Euler rotations + translation.
|
||||
The matrix is expected in row-major format (Blender convention).
|
||||
xform applies transforms right-to-left, so we write: -rx X -ry Y -rz Z -t tx ty tz
|
||||
"""
|
||||
from mathutils import Matrix as MMatrix
|
||||
|
||||
m = MMatrix(matrix)
|
||||
translation = m.to_translation()
|
||||
euler = m.to_euler("XYZ")
|
||||
|
||||
parts = []
|
||||
rx = math.degrees(euler.x)
|
||||
ry = math.degrees(euler.y)
|
||||
rz = math.degrees(euler.z)
|
||||
|
||||
if abs(rx) > 1e-6:
|
||||
parts.append(f"-rx {rx:.6f}")
|
||||
if abs(ry) > 1e-6:
|
||||
parts.append(f"-ry {ry:.6f}")
|
||||
if abs(rz) > 1e-6:
|
||||
parts.append(f"-rz {rz:.6f}")
|
||||
|
||||
parts.append(f"-t {translation.x:.6f} {translation.y:.6f} {translation.z:.6f}")
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs):
|
||||
try:
|
||||
output_bytes = pr.obj2mesh(inp, **kwargs)
|
||||
with open(output_file, "wb") as f:
|
||||
f.write(output_bytes)
|
||||
return output_file
|
||||
except Exception as e:
|
||||
print(f"ERROR in obj2mesh conversion:")
|
||||
print(f" Input file: {inp}")
|
||||
print(f" Output file: {output_file}")
|
||||
print(f" Additional args: {kwargs}")
|
||||
print(f" Error: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def convert_ies_to_radiance(
|
||||
ies_file_path: str,
|
||||
output_dir: str,
|
||||
lamp_type: str = "",
|
||||
lamp_color: tuple[float, float, float] = (1.0, 1.0, 1.0),
|
||||
multiply_factor: float = 1.0,
|
||||
radius: float = 0.0,
|
||||
) -> tuple[str, str]:
|
||||
"""Convert IES file to Radiance format using pyradiance.
|
||||
|
||||
Args:
|
||||
ies_file_path: Path to the .ies file
|
||||
output_dir: Directory where .rad and .dat files will be saved
|
||||
lamp_type: Type of lamp (e.g., 'LED', 'metal halide')
|
||||
lamp_color: RGB color tuple (0.0-1.0 each)
|
||||
multiply_factor: Brightness multiplier (0.1-10.0)
|
||||
radius: Illum sphere radius (0 = use IES geometry)
|
||||
|
||||
Returns:
|
||||
Tuple of (rad_file_path, dat_file_path)
|
||||
"""
|
||||
try:
|
||||
ies_path = Path(bpy.path.abspath(ies_file_path))
|
||||
base_name = ies_path.stem
|
||||
|
||||
output_path = os.path.join(output_dir, base_name)
|
||||
|
||||
kwargs = {"outname": output_path}
|
||||
|
||||
if lamp_type:
|
||||
kwargs["lamp_type"] = lamp_type
|
||||
|
||||
if lamp_color != (1.0, 1.0, 1.0):
|
||||
kwargs["lamp_color"] = lamp_color
|
||||
|
||||
if multiply_factor != 1.0:
|
||||
kwargs["multiply_factor"] = multiply_factor
|
||||
|
||||
if radius > 0.0:
|
||||
kwargs["radius"] = radius
|
||||
|
||||
pr.ies2rad(ies_path, **kwargs)
|
||||
|
||||
rad_file = os.path.join(output_dir, f"{base_name}.rad")
|
||||
dat_file = os.path.join(output_dir, f"{base_name}.dat")
|
||||
|
||||
return rad_file, dat_file
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to convert IES file '{ies_file_path}': {str(e)}")
|
||||
|
||||
|
||||
class PrepareRadianceScene(bpy.types.Operator):
|
||||
"""Prepares the Radiance scene (runs heavy work in background thread)"""
|
||||
|
||||
bl_idname = "scene.prepare_radiance"
|
||||
bl_label = "Prepare Radiance Scene"
|
||||
bl_description = "Prepares the Radiance scene by creating necessary files and setting up the view"
|
||||
|
||||
_timer = None
|
||||
_thread: Union[threading.Thread, None] = None
|
||||
_error: Union[str, None] = None
|
||||
_start_time: float = 0.0
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
if not props.output_dir:
|
||||
cls.poll_message_set("Output directory is not set.")
|
||||
return False
|
||||
if props.is_preparing:
|
||||
cls.poll_message_set("Scene preparation is already in progress.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_camera_data(self, camera):
|
||||
position = camera.matrix_world.to_translation()
|
||||
|
||||
direction = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
direction.normalize()
|
||||
|
||||
up = camera.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
up.normalize()
|
||||
|
||||
if abs(direction.dot(up)) > 0.99:
|
||||
if abs(direction.dot(Vector((0, 1, 0)))) > 0.99:
|
||||
reference = Vector((1, 0, 0))
|
||||
else:
|
||||
reference = Vector((0, 1, 0))
|
||||
right = direction.cross(reference)
|
||||
if right.length < 0.01:
|
||||
reference = Vector((0, 0, 1))
|
||||
right = direction.cross(reference)
|
||||
right.normalize()
|
||||
up = right.cross(direction)
|
||||
up.normalize()
|
||||
|
||||
if up.length < 0.01:
|
||||
up = Vector((0, 0, 1))
|
||||
else:
|
||||
up.normalize()
|
||||
|
||||
return (
|
||||
(position.x, position.y, position.z),
|
||||
(direction.x, direction.y, direction.z),
|
||||
(up.x, up.y, up.z),
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
print("Starting Radiance scene preparation...")
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
output_dir = props.output_dir
|
||||
|
||||
obj_file_path = os.path.join(output_dir, "model.obj")
|
||||
if not os.path.exists(obj_file_path):
|
||||
error_msg = "OBJ file not found. Please run 'Export Geometry for Simulation' first."
|
||||
self.report({"ERROR"}, error_msg)
|
||||
print(f"ERROR: {error_msg}")
|
||||
print(f" Expected file: {obj_file_path}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y
|
||||
|
||||
assert context.scene
|
||||
context.scene.render.resolution_x = resolution_x
|
||||
context.scene.render.resolution_y = resolution_y
|
||||
|
||||
aspect_ratio = resolution_x / resolution_y
|
||||
use_hdr = props.use_hdr
|
||||
choose_hdr_image = props.choose_hdr_image
|
||||
|
||||
print(f"Resolution: {resolution_x}x{resolution_y}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
print(f"Found OBJ file: {obj_file_path} ({os.path.getsize(obj_file_path)} bytes)")
|
||||
|
||||
hdr_image_path = ""
|
||||
hdr_mask_path = ""
|
||||
sky_map_cal_path = ""
|
||||
if use_hdr:
|
||||
hdr_image_path = os.path.join(os.path.dirname(__file__), "HDRs", "noon_grass_2k.hdr")
|
||||
hdr_mask_path = os.path.join(os.path.dirname(__file__), "HDRs", "noon_grass_2k_mask.hdr")
|
||||
sky_map_cal_path = os.path.join(os.path.dirname(__file__), "HDRs", "skymap.cal")
|
||||
|
||||
sky_file_path = os.path.join(output_dir, "sky.rad")
|
||||
|
||||
print("Setting up camera...")
|
||||
if props.use_active_camera:
|
||||
camera = context.scene.camera
|
||||
else:
|
||||
camera = props.selected_camera
|
||||
|
||||
if camera is None:
|
||||
self.report({"ERROR"}, "No active camera found in the scene. Please add a camera and set it as active.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
camera_position, camera_direction, camera_up = self.get_camera_data(camera)
|
||||
camera_type = camera.data.type
|
||||
camera_fov = camera.data.angle if camera_type == "PERSP" else 0.0
|
||||
camera_ortho_scale = camera.data.ortho_scale if camera_type == "ORTHO" else 0.0
|
||||
|
||||
print(f"Camera position: {camera_position}")
|
||||
print(f"Camera direction: {camera_direction}")
|
||||
print(f"Camera up: {camera_up}")
|
||||
|
||||
# Collect sky generation data (must be on main thread)
|
||||
sky_data = None
|
||||
if props.use_sun:
|
||||
sun_props = tool.Blender.get_solar_props()
|
||||
sun_pos_props = tool.Blender.get_sun_props()
|
||||
if not sun_pos_props:
|
||||
self.report(
|
||||
{"ERROR"}, "Sun position addon not available. Enable 'Sun Position' addon or disable 'Use Sun'."
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
sky_data = {
|
||||
"year": sun_props.year,
|
||||
"month": sun_props.month,
|
||||
"day": sun_props.day,
|
||||
"hour": sun_props.hour,
|
||||
"minute": sun_props.minute,
|
||||
"latitude": sun_props.latitude,
|
||||
"longitude": sun_props.longitude,
|
||||
"UTC_zone": sun_props.UTC_zone,
|
||||
"sun_year": sun_pos_props.year,
|
||||
"sky_condition": props.sky_condition,
|
||||
"ground_reflectance": props.ground_reflectance,
|
||||
"turbidity": props.turbidity,
|
||||
}
|
||||
|
||||
# Collect IES light data (must be on main thread)
|
||||
ies_light_data = []
|
||||
for idx, ies_light in enumerate(props.ies_lights):
|
||||
if not ies_light.is_enabled or not ies_light.ies_file_path:
|
||||
ies_light_data.append(None)
|
||||
continue
|
||||
|
||||
target_empties = ies_light.get_target_empties()
|
||||
if not target_empties:
|
||||
ies_light_data.append(None)
|
||||
continue
|
||||
|
||||
positions = []
|
||||
for obj in target_empties:
|
||||
try:
|
||||
positions.append((obj.location.x, obj.location.y, obj.location.z))
|
||||
except ReferenceError:
|
||||
continue
|
||||
|
||||
if not positions:
|
||||
ies_light_data.append(None)
|
||||
continue
|
||||
|
||||
ies_light_data.append(
|
||||
{
|
||||
"ies_file_path": ies_light.ies_file_path,
|
||||
"lamp_type": ies_light.lamp_type,
|
||||
"lamp_color": ies_light.lamp_color,
|
||||
"multiply_factor": ies_light.multiply_factor,
|
||||
"radius": ies_light.radius,
|
||||
"rotation_z": ies_light.rotation_z,
|
||||
"positions": positions,
|
||||
"is_enabled": ies_light.is_enabled,
|
||||
}
|
||||
)
|
||||
|
||||
# Collect material mapping data (must be on main thread)
|
||||
material_mappings = []
|
||||
for m in props.materials:
|
||||
material_mappings.append(
|
||||
{
|
||||
"style_id": m.style_id,
|
||||
"is_mapped": m.is_mapped,
|
||||
"category": m.category,
|
||||
"subcategory": m.subcategory,
|
||||
}
|
||||
)
|
||||
|
||||
linked_exports_snapshot = list(linked_model_exports)
|
||||
|
||||
# All Blender data collected — now launch background thread
|
||||
props.is_preparing = True
|
||||
self._start_time = time.time()
|
||||
self._error = None
|
||||
|
||||
import bonsai.bim.module.light.shared as shared
|
||||
|
||||
shared.scene = None
|
||||
|
||||
self._thread = threading.Thread(
|
||||
target=self._prepare_worker,
|
||||
args=(
|
||||
output_dir,
|
||||
obj_file_path,
|
||||
sky_file_path,
|
||||
use_hdr,
|
||||
choose_hdr_image,
|
||||
hdr_image_path,
|
||||
hdr_mask_path,
|
||||
sky_map_cal_path,
|
||||
sky_data,
|
||||
ies_light_data,
|
||||
material_mappings,
|
||||
linked_exports_snapshot,
|
||||
camera_position,
|
||||
camera_direction,
|
||||
camera_up,
|
||||
camera_type,
|
||||
camera_fov,
|
||||
camera_ortho_scale,
|
||||
aspect_ratio,
|
||||
),
|
||||
daemon=True,
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
wm = context.window_manager
|
||||
self._timer = wm.event_timer_add(0.5, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
|
||||
self.report({"INFO"}, "Scene preparation started in background...")
|
||||
context.window.cursor_set("WAIT")
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _prepare_worker(
|
||||
self,
|
||||
output_dir,
|
||||
obj_file_path,
|
||||
sky_file_path,
|
||||
use_hdr,
|
||||
choose_hdr_image,
|
||||
hdr_image_path,
|
||||
hdr_mask_path,
|
||||
sky_map_cal_path,
|
||||
sky_data,
|
||||
ies_light_data,
|
||||
material_mappings,
|
||||
linked_exports_snapshot,
|
||||
camera_position,
|
||||
camera_direction,
|
||||
camera_up,
|
||||
camera_type,
|
||||
camera_fov,
|
||||
camera_ortho_scale,
|
||||
aspect_ratio,
|
||||
):
|
||||
"""Runs in a background thread — no Blender API calls allowed here."""
|
||||
try:
|
||||
self._do_prepare(
|
||||
output_dir,
|
||||
obj_file_path,
|
||||
sky_file_path,
|
||||
use_hdr,
|
||||
choose_hdr_image,
|
||||
hdr_image_path,
|
||||
hdr_mask_path,
|
||||
sky_map_cal_path,
|
||||
sky_data,
|
||||
ies_light_data,
|
||||
material_mappings,
|
||||
linked_exports_snapshot,
|
||||
camera_position,
|
||||
camera_direction,
|
||||
camera_up,
|
||||
camera_type,
|
||||
camera_fov,
|
||||
camera_ortho_scale,
|
||||
aspect_ratio,
|
||||
)
|
||||
except Exception as e:
|
||||
self._error = str(e)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
def _do_prepare(
|
||||
self,
|
||||
output_dir,
|
||||
obj_file_path,
|
||||
sky_file_path,
|
||||
use_hdr,
|
||||
choose_hdr_image,
|
||||
hdr_image_path,
|
||||
hdr_mask_path,
|
||||
sky_map_cal_path,
|
||||
sky_data,
|
||||
ies_light_data,
|
||||
material_mappings,
|
||||
linked_exports_snapshot,
|
||||
camera_position,
|
||||
camera_direction,
|
||||
camera_up,
|
||||
camera_type,
|
||||
camera_fov,
|
||||
camera_ortho_scale,
|
||||
aspect_ratio,
|
||||
):
|
||||
"""The actual preparation logic (no Blender API)."""
|
||||
import bonsai.bim.module.light.shared as shared
|
||||
|
||||
# --- Generate sky file ---
|
||||
if sky_data is not None:
|
||||
dt = datetime(sky_data["year"], sky_data["month"], sky_data["day"], sky_data["hour"], sky_data["minute"])
|
||||
|
||||
print(f"Sun position data for Radiance gensky:")
|
||||
print(f" DateTime: {dt}")
|
||||
print(f" Latitude: {sky_data['latitude']}°")
|
||||
print(f" Longitude: {sky_data['longitude']}°")
|
||||
|
||||
sky_condition = sky_data["sky_condition"]
|
||||
longitude_for_gensky = -sky_data["longitude"]
|
||||
timezone_for_gensky = -int(sky_data["UTC_zone"]) * 15
|
||||
|
||||
sky_description = pr.gensky(
|
||||
dt=dt,
|
||||
latitude=sky_data["latitude"],
|
||||
longitude=longitude_for_gensky,
|
||||
year=sky_data["sun_year"],
|
||||
timezone=timezone_for_gensky,
|
||||
sunny_with_sun=sky_condition == "SUNNY_WITH_SUN",
|
||||
sunny_without_sun=sky_condition == "SUNNY_WITHOUT_SUN",
|
||||
cloudy=sky_condition == "CLOUDY",
|
||||
ground_reflectance=sky_data["ground_reflectance"],
|
||||
turbidity=sky_data["turbidity"],
|
||||
)
|
||||
|
||||
sky_description_str = sky_description.decode("utf-8")
|
||||
|
||||
if use_hdr and choose_hdr_image == "Noon":
|
||||
with open(sky_file_path, "w") as f:
|
||||
f.write(sky_description_str)
|
||||
f.write("\n")
|
||||
f.write(
|
||||
'''void colorpict env_map
|
||||
7 red green blue "'''
|
||||
+ hdr_image_path
|
||||
+ '''" "'''
|
||||
+ sky_map_cal_path
|
||||
+ '''" map_u map_v
|
||||
0
|
||||
1 0.5
|
||||
|
||||
# This is a multiplier to colour balance the env map
|
||||
# In this case, it provides a rough ground luminance from 3k-5k
|
||||
env_map colorfunc env_colour
|
||||
4 100 100 100 .
|
||||
0
|
||||
0
|
||||
|
||||
# .37 .57 1.5 is measured from a HDRI image
|
||||
# It is multiplied by a factor such that grey(r,g,b) = 1
|
||||
skyfunc colorfunc sky_colour
|
||||
4 .64 .99 2.6 .
|
||||
0
|
||||
0
|
||||
|
||||
void mixpict composite
|
||||
7 env_colour sky_colour grey "'''
|
||||
+ hdr_mask_path
|
||||
+ '''" "'''
|
||||
+ sky_map_cal_path
|
||||
+ """" map_u map_v
|
||||
0
|
||||
2 0.5 1
|
||||
|
||||
composite glow env_map_glow
|
||||
0
|
||||
0
|
||||
4 1 1 1 0
|
||||
|
||||
env_map_glow source sky
|
||||
0
|
||||
0
|
||||
4 0 0 1 180
|
||||
|
||||
env_colour glow ground_glow
|
||||
0
|
||||
0
|
||||
4 1 1 1 0
|
||||
|
||||
ground_glow source ground
|
||||
0
|
||||
0
|
||||
4 0 0 -1 180"""
|
||||
)
|
||||
elif not use_hdr:
|
||||
with open(sky_file_path, "w") as f:
|
||||
f.write(sky_description_str)
|
||||
f.write("\n")
|
||||
f.write("skyfunc glow sky_glow\n0\n0\n4 .9 .9 1.15 0\n")
|
||||
f.write("sky_glow source sky\n0\n0\n4 0 0 1 180\n")
|
||||
f.write("skyfunc glow ground_glow\n0\n0\n4 1.4 .9 .6 0\n")
|
||||
f.write("ground_glow source ground\n0\n0\n4 0 0 -1 180\n")
|
||||
else:
|
||||
print("Skipping sky generation (use_sun is False)...")
|
||||
|
||||
# --- Write materials.rad ---
|
||||
materials_file = os.path.join(output_dir, "materials.rad")
|
||||
written_materials = set()
|
||||
|
||||
all_materials = set(ifc_materials)
|
||||
|
||||
with open(materials_file, "w") as file:
|
||||
default_materials = [
|
||||
"void plastic white\n0\n0\n5 0.8 0.8 0.8 0 0\n",
|
||||
]
|
||||
for material in default_materials:
|
||||
file.write(material)
|
||||
written_materials.add(material.split()[2])
|
||||
|
||||
for style_id in all_materials:
|
||||
mat_data = next((m for m in material_mappings if m["style_id"] == style_id), None)
|
||||
if mat_data and mat_data["is_mapped"]:
|
||||
category, subcategory = mat_data["category"], mat_data["subcategory"]
|
||||
if category in spectraldb and subcategory in spectraldb[category]:
|
||||
material_def = spectraldb[category][subcategory]
|
||||
material_name = material_def.split()[2]
|
||||
if material_name not in written_materials:
|
||||
file.write(material_def + "\n")
|
||||
written_materials.add(material_name)
|
||||
file.write(f"inherit alias {style_id} {material_name}\n")
|
||||
else:
|
||||
file.write(f"inherit alias {style_id} white\n")
|
||||
else:
|
||||
file.write(f"inherit alias {style_id} white\n")
|
||||
|
||||
print(f"Exported Materials Rad file to: {materials_file}")
|
||||
|
||||
# --- Convert IES light files ---
|
||||
print("Processing IES light files...")
|
||||
converted_ies_lights = {}
|
||||
for idx, ies_data in enumerate(ies_light_data):
|
||||
if ies_data is None:
|
||||
continue
|
||||
try:
|
||||
rad_file, dat_file = convert_ies_to_radiance(
|
||||
ies_data["ies_file_path"],
|
||||
output_dir,
|
||||
lamp_type=ies_data["lamp_type"],
|
||||
lamp_color=ies_data["lamp_color"],
|
||||
multiply_factor=ies_data["multiply_factor"],
|
||||
radius=ies_data["radius"],
|
||||
)
|
||||
converted_ies_lights[idx] = (rad_file, dat_file)
|
||||
print(f" Converted IES light {idx}: {Path(ies_data['ies_file_path']).name}")
|
||||
except Exception as e:
|
||||
print(f" ERROR converting IES light {idx}: {str(e)}")
|
||||
|
||||
# --- Convert OBJ to RTM ---
|
||||
print(f"OBJ file size: {os.path.getsize(obj_file_path)} bytes")
|
||||
print(f"Materials file size: {os.path.getsize(materials_file)} bytes")
|
||||
print(f"Converting OBJ to RTM format...")
|
||||
|
||||
rtm_file_path = os.path.join(output_dir, "model.rtm")
|
||||
mesh_file_path = save_obj2mesh_output(obj_file_path, rtm_file_path, matfiles=[materials_file])
|
||||
print(f"obj2mesh output: {mesh_file_path}")
|
||||
|
||||
# Convert linked model OBJs to RTM
|
||||
linked_rtm_files = []
|
||||
for link_idx, (link_obj_path, link_mtl_path, link_matrix) in enumerate(linked_exports_snapshot):
|
||||
if not os.path.exists(link_obj_path):
|
||||
print(f"Linked model OBJ not found: {link_obj_path}")
|
||||
continue
|
||||
link_rtm_path = os.path.join(output_dir, f"linked_{link_idx}.rtm")
|
||||
try:
|
||||
save_obj2mesh_output(link_obj_path, link_rtm_path, matfiles=[materials_file])
|
||||
linked_rtm_files.append((link_rtm_path, link_matrix))
|
||||
print(f"Converted linked model {link_idx} to RTM")
|
||||
except Exception as e:
|
||||
print(f"Failed to convert linked model {link_idx} to RTM: {e}")
|
||||
|
||||
# --- Write scene.rad ---
|
||||
scene_file = os.path.join(output_dir, "scene.rad")
|
||||
with open(scene_file, "w") as file:
|
||||
file.write('void mesh model\n1 "' + rtm_file_path + '"\n0\n0\n')
|
||||
file.write("\n")
|
||||
|
||||
if linked_rtm_files:
|
||||
file.write("\n# Linked Models\n")
|
||||
for link_idx, (link_rtm_path, link_matrix) in enumerate(linked_rtm_files):
|
||||
is_identity = all(
|
||||
abs(link_matrix[i][j] - (1.0 if i == j else 0.0)) < 1e-6 for i in range(4) for j in range(4)
|
||||
)
|
||||
|
||||
if is_identity:
|
||||
file.write(f'void mesh linked_{link_idx}\n1 "{link_rtm_path}"\n0\n0\n\n')
|
||||
print(f"Added linked model {link_idx} to scene (identity, inline mesh)")
|
||||
else:
|
||||
link_rad_path = os.path.join(output_dir, f"linked_{link_idx}.rad")
|
||||
with open(link_rad_path, "w") as link_file:
|
||||
link_file.write(f'void mesh linked_{link_idx}\n1 "{link_rtm_path}"\n0\n0\n')
|
||||
|
||||
xform_args = _matrix_to_xform_args(link_matrix)
|
||||
file.write(f'!xform {xform_args} "{link_rad_path}"\n')
|
||||
print(f"Added linked model {link_idx} to scene with xform: {xform_args}")
|
||||
|
||||
# IES light fixtures
|
||||
if ies_light_data:
|
||||
file.write("\n# IES Light Fixtures\n")
|
||||
for idx, ies_data in enumerate(ies_light_data):
|
||||
if ies_data is None:
|
||||
continue
|
||||
z_rot = math.degrees(ies_data["rotation_z"])
|
||||
|
||||
for pos in ies_data["positions"]:
|
||||
if idx in converted_ies_lights:
|
||||
rad_path = converted_ies_lights[idx][0]
|
||||
rad_filename = Path(rad_path).name
|
||||
file.write(f'!xform -rz {z_rot} -t {pos[0]} {pos[1]} {pos[2]} "{rad_filename}"\n')
|
||||
else:
|
||||
rad_base = Path(ies_data["ies_file_path"]).stem
|
||||
rad_filename = f"{rad_base}.rad"
|
||||
file.write(f'# !xform -rz {z_rot} -t {pos[0]} {pos[1]} {pos[2]} "{rad_filename}"\n')
|
||||
|
||||
print(f"Exported Scene file to: {scene_file}")
|
||||
|
||||
# --- Validate light sources ---
|
||||
has_sky = sky_data is not None
|
||||
has_ies_lights = len(converted_ies_lights) > 0
|
||||
|
||||
if not has_sky and not has_ies_lights:
|
||||
raise RuntimeError("No light sources available. Please enable 'Use Sun' or add and map IES light fixtures.")
|
||||
|
||||
# --- Build pr.Scene ---
|
||||
print("Setting up Radiance scene...")
|
||||
new_scene = pr.Scene("ascene")
|
||||
|
||||
material_path = os.path.join(output_dir, "materials.rad")
|
||||
scene_path = os.path.join(output_dir, "scene.rad")
|
||||
|
||||
new_scene.add_material(material_path)
|
||||
new_scene.add_surface(scene_path)
|
||||
|
||||
if has_sky:
|
||||
new_scene.add_source(sky_file_path)
|
||||
print(f"Added sky light source")
|
||||
|
||||
if has_ies_lights:
|
||||
print(f"Added {len(converted_ies_lights)} IES light source(s)")
|
||||
|
||||
print("Setting up view...")
|
||||
if camera_type == "PERSP":
|
||||
vertical_fov = 2 * math.atan(math.tan(camera_fov / 2) / aspect_ratio)
|
||||
aview = pr.create_default_view()
|
||||
aview.type = "v"
|
||||
aview.vp = camera_position
|
||||
aview.vdir = camera_direction
|
||||
aview.vu = camera_up
|
||||
aview.horiz = math.degrees(camera_fov)
|
||||
aview.vert = math.degrees(vertical_fov)
|
||||
else:
|
||||
view_width = camera_ortho_scale
|
||||
view_height = camera_ortho_scale / aspect_ratio
|
||||
aview = pr.create_default_view()
|
||||
aview.type = "l"
|
||||
aview.vp = camera_position
|
||||
aview.vdir = camera_direction
|
||||
aview.vu = camera_up
|
||||
aview.horiz = view_width
|
||||
aview.vert = view_height
|
||||
|
||||
new_scene.add_view(aview)
|
||||
|
||||
# Set the global scene reference (thread-safe assignment)
|
||||
shared.scene = new_scene
|
||||
print("Scene preparation complete.")
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == "TIMER":
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
# Thread finished — clean up
|
||||
self._cleanup_timer(context)
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
props.is_preparing = False
|
||||
context.window.cursor_set("DEFAULT")
|
||||
|
||||
if self._error:
|
||||
self.report({"ERROR"}, f"Scene preparation failed: {self._error}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
elapsed = time.time() - self._start_time
|
||||
self.report({"INFO"}, f"Radiance scene prepared successfully in {elapsed:.1f}s")
|
||||
print(f"Scene preparation completed in {elapsed:.2f} seconds")
|
||||
return {"FINISHED"}
|
||||
|
||||
elif event.type == "ESC":
|
||||
self._cleanup_timer(context)
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
props.is_preparing = False
|
||||
context.window.cursor_set("DEFAULT")
|
||||
self.report({"WARNING"}, "Scene preparation cannot be cancelled mid-operation")
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def _cleanup_timer(self, context):
|
||||
if self._timer is not None:
|
||||
context.window_manager.event_timer_remove(self._timer)
|
||||
self._timer = None
|
||||
@@ -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 datetime
|
||||
import json
|
||||
import os
|
||||
@@ -44,6 +43,7 @@ from bonsai.bim.module.light.data import SolarData
|
||||
from bonsai.bim.module.light.decorator import SolarDecorator
|
||||
|
||||
sun_position = tool.Blender.get_addon("sun_position")
|
||||
now = datetime.datetime.now()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||
spectraldb: dict[str, dict[str, str]] = json.load(f)
|
||||
@@ -72,8 +72,8 @@ def update_coordinates(self: "BIMSolarProperties", context: bpy.types.Context) -
|
||||
def update_latlong(self: "BIMSolarProperties", context: bpy.types.Context) -> None:
|
||||
sun_props = tool.Blender.get_sun_props()
|
||||
assert sun_props
|
||||
sun_props.longitude = self.longitude
|
||||
sun_props.latitude = self.latitude
|
||||
sun_props.longitude = sun_props.longitude
|
||||
sun_props.latitude = sun_props.latitude
|
||||
self["coordinates"] = sun_props.coordinates
|
||||
update_sun_path(self)
|
||||
|
||||
@@ -135,14 +135,6 @@ def update_resolution(self: "RadianceExporterProperties", context: bpy.types.Con
|
||||
context.scene.render.resolution_y = self.radiance_resolution_y
|
||||
|
||||
|
||||
def update_day(self: "BIMSolarProperties", context: bpy.types.Context) -> None:
|
||||
"""Clamp the day to the valid range for the current month/year."""
|
||||
max_day = calendar.monthrange(self.year, self.month)[1]
|
||||
if self.day > max_day:
|
||||
self["day"] = max_day
|
||||
update_sun_path(self)
|
||||
|
||||
|
||||
def update_sun_path(self: "BIMSolarProperties", context: Union[bpy.types.Context, None] = None) -> None:
|
||||
if not SolarData.is_loaded:
|
||||
SolarData.load()
|
||||
@@ -160,13 +152,9 @@ def update_sun_path(self: "BIMSolarProperties", context: Union[bpy.types.Context
|
||||
sun_props.sun_distance = self.sun_path_size
|
||||
sun_props.latitude = self.latitude
|
||||
sun_props.longitude = self.longitude
|
||||
# Clamp day to valid range for the current month/year
|
||||
max_day = calendar.monthrange(self.year, self.month)[1]
|
||||
day = min(self.day, max_day)
|
||||
|
||||
sun_props.year = self.year
|
||||
sun_props.month = self.month
|
||||
sun_props.day = day
|
||||
sun_props.day = self.day
|
||||
sun_props.time = self.hour + (self.minute / 60)
|
||||
# Preserve IFC sign convention
|
||||
sun_props.north_offset = self.true_north * -1
|
||||
@@ -193,11 +181,8 @@ def update_sun_path(self: "BIMSolarProperties", context: Union[bpy.types.Context
|
||||
)
|
||||
sun_vector = sun_position.sun_calc.get_sun_vector(azimuth, elevation) * sun_props.sun_distance
|
||||
props.sun_position = sun_vector
|
||||
|
||||
# Update Blender viewport light direction for shadow visualization
|
||||
# This coordinate transformation converts from sun_position addon's coordinate system
|
||||
# to Blender's display.light_direction coordinate system
|
||||
# Note: This only affects viewport shading, not the Radiance rendering
|
||||
# sun_vector.z = max(0, sun_vector.z)
|
||||
# Light direction is a bit weird?
|
||||
mat = Matrix(((-1.0, 0.0, 0.0, 0.0), (0.0, 0, 1.0, 0.0), (-0.0, -1.0, 0, 0.0), (0.0, 0.0, 0.0, 1.0))).inverted()
|
||||
rotation_euler = Euler((elevation - pi / 2, 0, -azimuth))
|
||||
rotation_quaternion = rotation_euler.to_quaternion()
|
||||
@@ -208,9 +193,6 @@ def update_sun_path(self: "BIMSolarProperties", context: Union[bpy.types.Context
|
||||
|
||||
assert bpy.context.scene
|
||||
assert bpy.context.scene.display
|
||||
# Set viewport light direction based on sun position
|
||||
# If sun is below horizon (z < 0), use default upward direction
|
||||
# Otherwise, use calculated direction from sun position
|
||||
if sun_vector.z < 0:
|
||||
bpy.context.scene.display.light_direction = mat @ Vector((0, 0, 1))
|
||||
else:
|
||||
@@ -241,106 +223,6 @@ class RadianceMaterial(PropertyGroup):
|
||||
color: tuple[float, float, float]
|
||||
|
||||
|
||||
class IESLight(PropertyGroup):
|
||||
"""Represents a mapping between an IES light file and scene Empty objects.
|
||||
|
||||
Supports two targeting modes:
|
||||
- Object mode: target a single Empty object
|
||||
- Collection mode: target all Empty objects in a collection
|
||||
"""
|
||||
|
||||
ies_file_path: StringProperty(
|
||||
name="IES File Path",
|
||||
description="Path to the IES luminaire data file",
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
)
|
||||
use_collection: BoolProperty(
|
||||
name="Use Collection",
|
||||
description="Apply this light to all Empty objects in a collection instead of a single object",
|
||||
default=False,
|
||||
)
|
||||
target_object: PointerProperty(
|
||||
type=bpy.types.Object,
|
||||
name="Target Object",
|
||||
description="Empty object where the light fixture will be placed",
|
||||
poll=lambda self, obj: obj.type == "EMPTY",
|
||||
)
|
||||
target_collection: PointerProperty(
|
||||
type=bpy.types.Collection,
|
||||
name="Target Collection",
|
||||
description="Collection of Empty objects where the light fixture will be placed",
|
||||
)
|
||||
rotation_z: FloatProperty(
|
||||
name="Rotation Z",
|
||||
description="Rotation around Z-axis in degrees (-180 to 180)",
|
||||
min=-180.0,
|
||||
max=180.0,
|
||||
default=0.0,
|
||||
subtype="ANGLE",
|
||||
)
|
||||
is_enabled: BoolProperty(
|
||||
name="Enabled",
|
||||
description="Include this light in the Radiance export",
|
||||
default=True,
|
||||
)
|
||||
|
||||
# Additional lamp properties for customization
|
||||
lamp_type: StringProperty(
|
||||
name="Lamp Type",
|
||||
description="Type of lamp (e.g., 'LED', 'metal halide', 'fluorescent')",
|
||||
default="",
|
||||
)
|
||||
lamp_color: FloatVectorProperty(
|
||||
name="Lamp Color",
|
||||
description="Lamp color (RGB) for custom color adjustments",
|
||||
subtype="COLOR",
|
||||
default=(1.0, 1.0, 1.0),
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=3,
|
||||
)
|
||||
multiply_factor: FloatProperty(
|
||||
name="Brightness Factor",
|
||||
description="Multiply all output quantities by this factor (0.1 to 10.0)",
|
||||
min=0.1,
|
||||
max=10.0,
|
||||
default=1.0,
|
||||
)
|
||||
radius: FloatProperty(
|
||||
name="Illum Sphere Radius",
|
||||
description="Radius of illum sphere (ignores geometry from IES file). 0 = use IES geometry",
|
||||
min=0.0,
|
||||
max=10.0,
|
||||
default=0.0,
|
||||
)
|
||||
|
||||
def get_target_empties(self) -> list[bpy.types.Object]:
|
||||
"""Return all Empty objects this light targets."""
|
||||
if self.use_collection and self.target_collection is not None:
|
||||
return [obj for obj in self.target_collection.all_objects if obj.type == "EMPTY" and not obj.hide_get()]
|
||||
elif not self.use_collection and self.target_object is not None:
|
||||
try:
|
||||
_ = self.target_object.name
|
||||
if not self.target_object.hide_get():
|
||||
return [self.target_object]
|
||||
except ReferenceError:
|
||||
pass
|
||||
return []
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ies_file_path: str
|
||||
use_collection: bool
|
||||
target_object: Union[bpy.types.Object, None]
|
||||
target_collection: Union[bpy.types.Collection, None]
|
||||
rotation_z: float
|
||||
is_enabled: bool
|
||||
lamp_type: str
|
||||
lamp_color: tuple[float, float, float]
|
||||
multiply_factor: float
|
||||
radius: float
|
||||
|
||||
|
||||
class RadianceExporterProperties(PropertyGroup):
|
||||
|
||||
def update_output_dir(self, context) -> None:
|
||||
@@ -351,9 +233,6 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
if self.ifc_file:
|
||||
self.ifc_file = bpy.path.abspath(self.ifc_file)
|
||||
|
||||
def get_categories(self, context):
|
||||
return sorted([(k, k, "") for k in spectraldb.keys()])
|
||||
|
||||
def add_material_mapping(self, style_id: str, style_name: str) -> RadianceMaterial:
|
||||
item = self.materials.add()
|
||||
item.name = style_name
|
||||
@@ -368,10 +247,7 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
mappings = json.load(f)
|
||||
|
||||
for style_id, mapping in mappings.items():
|
||||
# Try matching by style_id first, fall back to name
|
||||
material = self.get_material_mapping_by_id(style_id)
|
||||
if material is None:
|
||||
material = self.get_material_mapping(mapping["name"])
|
||||
material = self.get_material_mapping(mapping["name"])
|
||||
if material:
|
||||
material.style_id = style_id
|
||||
material.category = mapping["category"]
|
||||
@@ -383,14 +259,11 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
new_material.subcategory = mapping["subcategory"]
|
||||
new_material.is_mapped = True
|
||||
|
||||
def get_material_mapping_by_id(self, style_id: str) -> Union[RadianceMaterial, None]:
|
||||
return next((item for item in self.materials if item.style_id == style_id), None)
|
||||
|
||||
def get_material_mapping(self, style_name: str) -> Union[RadianceMaterial, None]:
|
||||
return next((item for item in self.materials if item.name == style_name), None)
|
||||
|
||||
def set_material_mapping(self, style_id: str, style_name: str, category: str, subcategory: str) -> None:
|
||||
item = self.get_material_mapping_by_id(style_id) or self.get_material_mapping(style_name)
|
||||
item = self.get_material_mapping(style_name)
|
||||
if item:
|
||||
item.category = category
|
||||
item.subcategory = subcategory
|
||||
@@ -406,12 +279,8 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
if item.category and item.subcategory
|
||||
}
|
||||
|
||||
def unmap_material(self, style_name: str, style_id: str = "") -> None:
|
||||
item = None
|
||||
if style_id:
|
||||
item = self.get_material_mapping_by_id(style_id)
|
||||
if item is None:
|
||||
item = self.get_material_mapping(style_name)
|
||||
def unmap_material(self, style_name: str) -> None:
|
||||
item = self.get_material_mapping(style_name)
|
||||
if item:
|
||||
item.category = ""
|
||||
item.subcategory = ""
|
||||
@@ -421,14 +290,6 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
name="Is Exporting", description="Whether the OBJ export is in progress", default=False
|
||||
)
|
||||
|
||||
is_preparing: bpy.props.BoolProperty(
|
||||
name="Is Preparing", description="Whether scene preparation is in progress", default=False
|
||||
)
|
||||
|
||||
is_rendering: bpy.props.BoolProperty(
|
||||
name="Is Rendering", description="Whether a Radiance render is in progress", default=False
|
||||
)
|
||||
|
||||
categories = [
|
||||
("Wall", "Wall", ""),
|
||||
("Floor", "Floor", ""),
|
||||
@@ -455,13 +316,16 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
print(f"Material '{active_material.name}' mapped to {self.category} - {self.subcategory}")
|
||||
|
||||
category: bpy.props.EnumProperty(
|
||||
items=get_categories, name="Category", description="Material category", update=update_material_mapping
|
||||
items=categories, name="Category", description="Material category", update=update_material_mapping
|
||||
)
|
||||
|
||||
def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global SUBCATEGORIES_ENUM_ITEMS
|
||||
if self.category in spectraldb:
|
||||
return sorted([(k, k, "") for k in spectraldb[self.category].keys()])
|
||||
return []
|
||||
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
|
||||
else:
|
||||
SUBCATEGORIES_ENUM_ITEMS = []
|
||||
return SUBCATEGORIES_ENUM_ITEMS
|
||||
|
||||
subcategory: bpy.props.EnumProperty(
|
||||
items=get_subcategories, name="Subcategory", description="Material subcategory", update=update_material_mapping
|
||||
@@ -470,9 +334,12 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
materials: CollectionProperty(type=RadianceMaterial)
|
||||
active_material_index: IntProperty()
|
||||
|
||||
# material_mappings: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup, name="Material Mappings")
|
||||
# material_mappings: CollectionProperty(type=MaterialMapping)
|
||||
|
||||
should_load_from_memory: BoolProperty(
|
||||
name="Load from Memory",
|
||||
default=True,
|
||||
default=False,
|
||||
)
|
||||
|
||||
radiance_resolution_x: IntProperty(
|
||||
@@ -481,13 +348,6 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
radiance_resolution_y: IntProperty(
|
||||
name="Y", description="Vertical resolution of the output image", default=1080, min=1, update=update_resolution
|
||||
)
|
||||
radiance_bin_dir: StringProperty(
|
||||
name="Radiance Bin",
|
||||
description="Path to the Radiance bin folder (containing falsecolor, pcomb, etc.)",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
)
|
||||
|
||||
output_dir: StringProperty(
|
||||
name="Output Directory",
|
||||
description="Directory to output Radiance files",
|
||||
@@ -503,15 +363,6 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
update=lambda self, context: self.update_ifc_file(context),
|
||||
)
|
||||
|
||||
ambient_bounces: IntProperty(
|
||||
name="Ambient Bounces",
|
||||
description="Number of indirect light bounces. Higher = more light fills dark areas but slower. "
|
||||
"1 is minimal, 2-3 recommended for IES-only scenes, 4+ for complex interiors",
|
||||
min=0,
|
||||
max=8,
|
||||
default=2,
|
||||
)
|
||||
|
||||
radiance_quality: EnumProperty(
|
||||
name="Quality",
|
||||
description="Radiance rendering quality",
|
||||
@@ -542,15 +393,14 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
name="Output File Format",
|
||||
description="Format of the output image file",
|
||||
items=[
|
||||
("HDR", "HDR", "High Dynamic Range (HDR) file only"),
|
||||
("HDR_TIFF", "HDR + Tiff", "High Dynamic Range (HDR) and Tiff files"),
|
||||
("HDR", "HDR + Tiff", "High Dynamic Range"),
|
||||
],
|
||||
default="HDR_TIFF",
|
||||
default="HDR",
|
||||
)
|
||||
|
||||
use_hdr: BoolProperty(
|
||||
name="HDR Environment Map",
|
||||
description="Use an HDR image as the sky dome for realistic environment lighting and reflections",
|
||||
name="Use HDR",
|
||||
description="Use HDR image format",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@@ -563,40 +413,6 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
default="Noon",
|
||||
)
|
||||
|
||||
use_sun: BoolProperty(
|
||||
name="Use Sun",
|
||||
description="Use sun position data to generate sky. If disabled, generates a default sky without sun",
|
||||
default=True,
|
||||
)
|
||||
|
||||
# Sky generation parameters
|
||||
sky_condition: EnumProperty(
|
||||
name="Sky Condition",
|
||||
description="Type of sky condition to generate",
|
||||
items=[
|
||||
("SUNNY_WITH_SUN", "Sunny with Sun", "Clear sky with direct sun"),
|
||||
("SUNNY_WITHOUT_SUN", "Sunny without Sun", "Clear sky without direct sun"),
|
||||
("CLOUDY", "Cloudy", "Overcast sky condition"),
|
||||
],
|
||||
default="SUNNY_WITH_SUN",
|
||||
)
|
||||
|
||||
ground_reflectance: FloatProperty(
|
||||
name="Ground Reflectance",
|
||||
description="Ground reflectance value (0.0 to 1.0)",
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
default=0.2,
|
||||
)
|
||||
|
||||
turbidity: FloatProperty(
|
||||
name="Turbidity",
|
||||
description="Atmospheric turbidity (1.0 to 10.0). Lower values = clearer sky",
|
||||
min=1.0,
|
||||
max=10.0,
|
||||
default=3.0,
|
||||
)
|
||||
|
||||
use_active_camera: BoolProperty(
|
||||
name="Use Active Camera", description="Use the active camera in the scene", default=True
|
||||
)
|
||||
@@ -608,90 +424,8 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
poll=lambda self, object: object.type == "CAMERA",
|
||||
)
|
||||
|
||||
ies_lights: CollectionProperty(
|
||||
type=IESLight,
|
||||
name="IES Lights",
|
||||
description="Collection of IES light fixtures mapped to scene objects",
|
||||
)
|
||||
active_ies_light_index: IntProperty(
|
||||
name="Active IES Light Index",
|
||||
description="Index of the active IES light in the collection",
|
||||
default=-1,
|
||||
)
|
||||
|
||||
# False color analysis properties
|
||||
use_false_color: BoolProperty(
|
||||
name="Generate False Color Image",
|
||||
description="Generate a false color HDR image for illuminance analysis",
|
||||
default=False,
|
||||
)
|
||||
|
||||
false_color_label: EnumProperty(
|
||||
name="Legend Label Unit",
|
||||
description="Unit for the false color legend",
|
||||
items=[
|
||||
("fc", "Foot-Candles", "US lighting standard unit"),
|
||||
("lux", "Lux", "International lighting standard unit"),
|
||||
("cd/m2", "Candela per m²", "Luminance unit"),
|
||||
],
|
||||
default="fc",
|
||||
)
|
||||
|
||||
false_color_scale: FloatProperty(
|
||||
name="Scale Factor",
|
||||
description="Maximum scale value for the false color legend",
|
||||
min=0.1,
|
||||
max=100000.0,
|
||||
default=3.0,
|
||||
)
|
||||
|
||||
false_color_steps: IntProperty(
|
||||
name="Legend Steps",
|
||||
description="Number of divisions on the legend. Contour increment = Scale / Steps. "
|
||||
"E.g. Scale=20, Steps=10 → contours at 2, 4, 6, 8...",
|
||||
min=2,
|
||||
max=50,
|
||||
default=10,
|
||||
)
|
||||
|
||||
false_color_contour_lines: BoolProperty(
|
||||
name="Enable Contour Lines",
|
||||
description="Add contour lines to the false color image",
|
||||
default=True,
|
||||
)
|
||||
|
||||
false_color_contour_mode: EnumProperty(
|
||||
name="Contour Mode",
|
||||
description="Contour line display mode",
|
||||
items=[
|
||||
(
|
||||
"WITH_BG",
|
||||
"Contour Lines with Background",
|
||||
"Show contour lines overlaid on the colored false color background",
|
||||
),
|
||||
("WITHOUT_BG", "Contour Lines Only", "Show only contour lines without the false color background"),
|
||||
],
|
||||
default="WITH_BG",
|
||||
)
|
||||
|
||||
false_color_multiplier: FloatProperty(
|
||||
name="Multiplier",
|
||||
description="Conversion multiplier (179.0 for lux, 16.6295 for foot-candles)",
|
||||
min=0.1,
|
||||
max=1000.0,
|
||||
default=16.629505759940542,
|
||||
)
|
||||
|
||||
false_color_output_name: StringProperty(
|
||||
name="False Color Output Name",
|
||||
description="Name of the false color output file (without extension)",
|
||||
default="false_color",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_exporting: bool
|
||||
is_preparing: bool
|
||||
is_rendering: bool
|
||||
category: str
|
||||
subcategory: str
|
||||
materials: bpy.types.bpy_prop_collection_idprop[RadianceMaterial]
|
||||
@@ -699,10 +433,8 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
should_load_from_memory: bool
|
||||
radiance_resolution_x: int
|
||||
radiance_resolution_y: int
|
||||
radiance_bin_dir: str
|
||||
output_dir: str
|
||||
ifc_file: str
|
||||
ambient_bounces: int
|
||||
radiance_quality: Literal["LOW", "MEDIUM", "HIGH"]
|
||||
radiance_detail: Literal["LOW", "MEDIUM", "HIGH"]
|
||||
radiance_variability: Literal["LOW", "MEDIUM", "HIGH"]
|
||||
@@ -710,22 +442,8 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
output_file_format: Literal["HDR"]
|
||||
use_hdr: bool
|
||||
choose_hdr_image: Literal["Noon"]
|
||||
use_sun: bool
|
||||
sky_condition: Literal["SUNNY_WITH_SUN", "SUNNY_WITHOUT_SUN", "CLOUDY"]
|
||||
ground_reflectance: float
|
||||
turbidity: float
|
||||
use_active_camera: bool
|
||||
selected_camera: Union[bpy.types.Object, None]
|
||||
ies_lights: bpy.types.bpy_prop_collection_idprop[IESLight]
|
||||
active_ies_light_index: int
|
||||
use_false_color: bool
|
||||
false_color_label: Literal["fc", "lux", "cd/m2"]
|
||||
false_color_scale: float
|
||||
false_color_steps: int
|
||||
false_color_contour_lines: bool
|
||||
false_color_contour_mode: Literal["WITH_BG", "WITHOUT_BG"]
|
||||
false_color_multiplier: float
|
||||
false_color_output_name: str
|
||||
|
||||
|
||||
class BIMSolarProperties(PropertyGroup):
|
||||
@@ -752,12 +470,11 @@ class BIMSolarProperties(PropertyGroup):
|
||||
)
|
||||
timezone: StringProperty(name="Timezone", default="Etc/GMT")
|
||||
true_north: FloatProperty(name="True North", min=-pi, max=pi, subtype="ANGLE", update=update_sun_path)
|
||||
# Defaults are static; use the "Now" button (LightSetTimeToNow) to set current time.
|
||||
year: IntProperty(name="Year", min=1, max=9999, default=2025, update=update_day)
|
||||
month: IntProperty(name="Month", min=1, max=12, default=1, update=update_day)
|
||||
day: IntProperty(name="Date", min=1, max=31, default=1, update=update_day)
|
||||
hour: IntProperty(name="Hour", min=0, max=23, default=12, update=update_sun_path)
|
||||
minute: IntProperty(name="Minute", min=0, max=59, default=0, update=update_sun_path)
|
||||
year: IntProperty(name="Year", min=1, max=9999, default=now.year, update=update_sun_path)
|
||||
month: IntProperty(name="Month", min=1, max=12, default=now.month, update=update_sun_path)
|
||||
day: IntProperty(name="Date", min=1, max=31, default=now.day, update=update_sun_path)
|
||||
hour: IntProperty(name="Hour", min=0, max=23, default=now.hour, update=update_sun_path)
|
||||
minute: IntProperty(name="Minute", min=0, max=59, default=now.minute, update=update_sun_path)
|
||||
sun_position: FloatVectorProperty(name="Sun Position", subtype="XYZ", default=(0, 0, 0))
|
||||
sun_path_origin: FloatVectorProperty(name="Sun Path Origin", subtype="XYZ", default=(0, 0, 0))
|
||||
sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path)
|
||||
|
||||
@@ -1,328 +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/>.
|
||||
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import bpy
|
||||
import pyradiance as pr
|
||||
|
||||
import bonsai.bim.module.light.shared as shared
|
||||
import bonsai.tool as tool
|
||||
|
||||
# pyradiance's bundled Radiance binaries
|
||||
_PYRAD_BIN = Path(pr.__file__).parent / "bin"
|
||||
|
||||
|
||||
class RadianceRender(bpy.types.Operator):
|
||||
"""Radiance Rendering (runs in background thread)"""
|
||||
|
||||
bl_idname = "render_scene.radiance"
|
||||
bl_label = "Render"
|
||||
bl_description = "Renders the scene using Radiance"
|
||||
|
||||
_timer = None
|
||||
_thread: Union[threading.Thread, None] = None
|
||||
_result_image: Union[bytes, None] = None
|
||||
_error: Union[str, None] = None
|
||||
_start_time: float = 0.0
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
if not props.output_dir:
|
||||
cls.poll_message_set("Output directory is not set.")
|
||||
return False
|
||||
if props.is_rendering:
|
||||
cls.poll_message_set("A render is already in progress.")
|
||||
return False
|
||||
if shared.scene is None:
|
||||
cls.poll_message_set("Radiance scene not prepared. Please run 'Prepare Scene' (Step 2) first.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y
|
||||
|
||||
context.scene.render.resolution_x = resolution_x
|
||||
context.scene.render.resolution_y = resolution_y
|
||||
|
||||
quality = props.radiance_quality.upper()
|
||||
detail = props.radiance_detail.upper()
|
||||
variability = props.radiance_variability.upper()
|
||||
ambient_bounces = props.ambient_bounces
|
||||
output_dir = props.output_dir
|
||||
|
||||
props.is_rendering = True
|
||||
self._start_time = time.time()
|
||||
self._result_image = None
|
||||
self._error = None
|
||||
|
||||
self._thread = threading.Thread(
|
||||
target=self._render_worker,
|
||||
args=(shared.scene, output_dir, resolution_x, resolution_y, quality, detail, variability, ambient_bounces),
|
||||
daemon=True,
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
wm = context.window_manager
|
||||
self._timer = wm.event_timer_add(0.5, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
|
||||
self.report({"INFO"}, "Radiance render started in background...")
|
||||
context.window.cursor_set("WAIT")
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _render_worker(self, render_scene, output_dir, res_x, res_y, quality, detail, variability, ambient_bounces):
|
||||
"""Runs in a background thread — no Blender API calls allowed here."""
|
||||
cwd_saved = os.getcwd()
|
||||
try:
|
||||
os.chdir(output_dir)
|
||||
self._result_image = pr.render(
|
||||
render_scene,
|
||||
ambbounce=ambient_bounces,
|
||||
resolution=(res_x, res_y),
|
||||
quality=quality,
|
||||
detail=detail,
|
||||
variability=variability,
|
||||
nproc=multiprocessing.cpu_count(),
|
||||
)
|
||||
except Exception as e:
|
||||
self._error = str(e)
|
||||
finally:
|
||||
os.chdir(cwd_saved)
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == "TIMER":
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
self._cleanup_timer(context)
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
props.is_rendering = False
|
||||
context.window.cursor_set("DEFAULT")
|
||||
|
||||
if self._error:
|
||||
self.report({"ERROR"}, f"Radiance render failed: {self._error}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
elapsed = time.time() - self._start_time
|
||||
print(f"Render completed in {elapsed:.2f} seconds")
|
||||
|
||||
output_dir = props.output_dir
|
||||
output_file_name = props.output_file_name
|
||||
output_file_format = props.output_file_format
|
||||
|
||||
output_hdr_path = os.path.join(output_dir, f"{output_file_name}.hdr")
|
||||
print(f"Saving HDR output to: {output_hdr_path}")
|
||||
with open(output_hdr_path, "wb") as wtr:
|
||||
wtr.write(self._result_image)
|
||||
|
||||
if output_file_format == "HDR_TIFF":
|
||||
print("Applying tone mapping...")
|
||||
pcond_image = pr.pcond(hdr=output_hdr_path, human=True)
|
||||
tiff_path = os.path.join(output_dir, f"{output_file_name}.tiff")
|
||||
print(f"Saving TIFF output to: {tiff_path}")
|
||||
pr.ra_tiff(inp=pcond_image, out=tiff_path, lzw=True)
|
||||
|
||||
print("Radiance rendering process completed successfully.")
|
||||
self.report({"INFO"}, f"Radiance rendering completed. HDR Output: {output_hdr_path}")
|
||||
if output_file_format == "HDR_TIFF":
|
||||
self.report({"INFO"}, f"TIFF Output: {tiff_path}")
|
||||
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
elif event.type == "ESC":
|
||||
self._cleanup_timer(context)
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
props.is_rendering = False
|
||||
context.window.cursor_set("DEFAULT")
|
||||
self.report({"WARNING"}, "Render cancelled by user. Background process may still be running.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def _cleanup_timer(self, context):
|
||||
if self._timer is not None:
|
||||
context.window_manager.event_timer_remove(self._timer)
|
||||
self._timer = None
|
||||
|
||||
|
||||
class FalseColorRadiance(bpy.types.Operator):
|
||||
"""Generate false color HDR image for illuminance analysis"""
|
||||
|
||||
bl_idname = "render_scene.false_color_radiance"
|
||||
bl_label = "Generate False Color Image"
|
||||
bl_description = "Generate a false color HDR image for illuminance analysis"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
if not props.output_dir:
|
||||
cls.poll_message_set("Output directory is not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
output_dir = props.output_dir
|
||||
output_file_name = props.output_file_name
|
||||
|
||||
hdr_path = os.path.join(output_dir, f"{output_file_name}.hdr")
|
||||
if not os.path.exists(hdr_path):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"HDR file not found at: {hdr_path}. Please run 'Radiance Render' first.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
fc_scale = (
|
||||
str(int(props.false_color_scale))
|
||||
if props.false_color_scale == int(props.false_color_scale)
|
||||
else str(props.false_color_scale)
|
||||
)
|
||||
|
||||
# Multiplier converts Radiance raw values to display units
|
||||
# fc (foot-candles) = 16.629..., lux & cd/m2 = 179.0
|
||||
multiplier = 179.0 if props.false_color_label in ("lux", "cd/m2") else 16.629505759940542
|
||||
|
||||
print(
|
||||
f"False color parameters: label={props.false_color_label}, scale={fc_scale}, "
|
||||
f"steps={props.false_color_steps}, multiplier={multiplier}, "
|
||||
f"contour={props.false_color_contour_lines}"
|
||||
)
|
||||
|
||||
try:
|
||||
fc_output_name = props.false_color_output_name
|
||||
fc_hdr_path = os.path.join(output_dir, f"{fc_output_name}.hdr")
|
||||
|
||||
# Find falsecolor binary from user-specified Radiance bin directory
|
||||
radiance_bin_dir = os.path.normpath(props.radiance_bin_dir) if props.radiance_bin_dir else ""
|
||||
if radiance_bin_dir:
|
||||
falsecolor_bin = os.path.join(radiance_bin_dir, "falsecolor.exe")
|
||||
if not os.path.exists(falsecolor_bin):
|
||||
falsecolor_bin = os.path.join(radiance_bin_dir, "falsecolor")
|
||||
if not os.path.exists(falsecolor_bin):
|
||||
self.report({"ERROR"}, f"falsecolor not found in: {radiance_bin_dir}")
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
import shutil
|
||||
|
||||
falsecolor_bin = shutil.which("falsecolor") or shutil.which("falsecolor.exe")
|
||||
if not falsecolor_bin:
|
||||
self.report(
|
||||
{"ERROR"}, "Set the Radiance Bin path in False Color settings, or add Radiance to system PATH."
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
radiance_bin_dir = os.path.dirname(falsecolor_bin)
|
||||
|
||||
radiance_lib = os.path.join(os.path.dirname(radiance_bin_dir), "lib")
|
||||
print(f"Using falsecolor: {falsecolor_bin}")
|
||||
print(f"Radiance bin: {radiance_bin_dir}, lib: {radiance_lib}")
|
||||
|
||||
cmd = [falsecolor_bin]
|
||||
cmd.extend(["-m", str(multiplier)])
|
||||
cmd.extend(["-s", fc_scale])
|
||||
cmd.extend(["-n", str(props.false_color_steps)])
|
||||
cmd.extend(["-l", props.false_color_label])
|
||||
|
||||
if props.false_color_contour_lines:
|
||||
cmd.append("-cl")
|
||||
if props.false_color_contour_mode == "WITH_BG":
|
||||
cmd.extend(["-ip", hdr_path])
|
||||
else:
|
||||
cmd.extend(["-i", hdr_path])
|
||||
else:
|
||||
cmd.extend(["-ip", hdr_path])
|
||||
|
||||
# Setup environment so falsecolor can find pcomb, psign, pcompos etc.
|
||||
env = os.environ.copy()
|
||||
if os.path.exists(radiance_bin_dir):
|
||||
env["PATH"] = radiance_bin_dir + os.pathsep + env.get("PATH", "")
|
||||
if os.path.exists(radiance_lib):
|
||||
env["RAYPATH"] = "." + os.pathsep + radiance_lib
|
||||
|
||||
print(f"Running: {' '.join(cmd)}")
|
||||
# Write output to file via redirection to avoid Windows stdout binary corruption
|
||||
cmd_str = subprocess.list2cmdline(cmd) + f' > "{fc_hdr_path}"'
|
||||
result = subprocess.run(cmd_str, shell=True, stderr=subprocess.PIPE, env=env, cwd=output_dir)
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr.decode() if result.stderr else "Unknown error"
|
||||
self.report({"ERROR"}, f"falsecolor failed: {error_msg}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
fc_size = os.path.getsize(fc_hdr_path) if os.path.exists(fc_hdr_path) else 0
|
||||
print(f"False color HDR generated: {fc_hdr_path} ({fc_size} bytes)")
|
||||
self.report({"INFO"}, f"False color image generated: {fc_hdr_path}")
|
||||
|
||||
# Generate TIFF version
|
||||
try:
|
||||
pcond_fc_image = pr.pcond(hdr=fc_hdr_path, human=True)
|
||||
fc_tiff_path = os.path.join(output_dir, f"{fc_output_name}.tiff")
|
||||
pr.ra_tiff(inp=pcond_fc_image, out=fc_tiff_path, lzw=True)
|
||||
print(f"False color TIFF generated: {fc_tiff_path}")
|
||||
self.report({"INFO"}, f"False color TIFF also generated: {fc_tiff_path}")
|
||||
except Exception as e:
|
||||
self.report({"WARNING"}, f"TIFF generation failed: {str(e)}")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Clean up incomplete HDR file on failure
|
||||
if os.path.exists(fc_hdr_path):
|
||||
os.remove(fc_hdr_path)
|
||||
error_msg = f"falsecolor failed: {e.stderr.decode() if e.stderr else str(e)}"
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
except FileNotFoundError:
|
||||
self.report({"ERROR"}, "falsecolor not found. Please install Radiance and add it to PATH.")
|
||||
return {"CANCELLED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to generate false color image: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class RADIANCE_OT_select_camera(bpy.types.Operator):
|
||||
bl_idname = "radiance.select_camera"
|
||||
bl_label = "Select Camera"
|
||||
bl_description = "Select a camera from the viewport"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object is not None and context.object.type == "CAMERA"
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
props.selected_camera = context.object
|
||||
props.use_active_camera = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1,33 +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 module-level state for the light/radiance pipeline.
|
||||
|
||||
These globals are populated by ExportOBJ, consumed by PrepareRadianceScene,
|
||||
and read by RadianceRender. They must live in a shared module so that
|
||||
all operator files can access the same state.
|
||||
"""
|
||||
|
||||
# Collected IFC material names from the most recent export
|
||||
ifc_materials: list[str] = []
|
||||
|
||||
# The prepared pyradiance Scene object (set by PrepareRadianceScene, read by RadianceRender)
|
||||
scene = None
|
||||
|
||||
# Info about exported linked models: list of (obj_path, mtl_path, link_matrix_4x4)
|
||||
linked_model_exports: list[tuple[str, str, list[list[float]]]] = []
|
||||
@@ -1,148 +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/>.
|
||||
|
||||
import webbrowser
|
||||
from datetime import datetime
|
||||
from math import radians
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.geolocation
|
||||
import requests
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.light.data import SolarData
|
||||
|
||||
|
||||
class ImportTrueNorth(bpy.types.Operator):
|
||||
bl_idname = "bim.import_true_north"
|
||||
bl_label = "Import True North"
|
||||
bl_description = "Imports the True North from your IFC geometric context"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
if not SolarData.is_loaded:
|
||||
SolarData.load()
|
||||
return SolarData.data["true_north"] is not None
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
if not context.TrueNorth:
|
||||
continue
|
||||
value = context.TrueNorth.DirectionRatios
|
||||
props.true_north = radians(ifcopenshell.util.geolocation.yaxis2angle(*value[:2]))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImportLatLong(bpy.types.Operator):
|
||||
bl_idname = "bim.import_lat_long"
|
||||
bl_label = "Import Latitude / Longitude"
|
||||
bl_description = "Imports the latitude / longitude from an IfcSite"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
site = tool.Ifc.get().by_id(int(props.sites))
|
||||
if site.RefLatitude and site.RefLongitude:
|
||||
props.latitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLatitude)
|
||||
props.longitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLongitude)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class MoveSunPathTo3DCursor(bpy.types.Operator):
|
||||
bl_idname = "bim.move_sun_path_to_3d_cursor"
|
||||
bl_label = "Move Sun Path To 3D Cursor"
|
||||
bl_description = "Shifts the visualisation of the Sun Path to the 3D cursor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
assert context.scene
|
||||
props.sun_path_origin = context.scene.cursor.location
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ViewFromSun(bpy.types.Operator):
|
||||
bl_idname = "bim.view_from_sun"
|
||||
bl_label = "View From Sun"
|
||||
bl_description = "Views your model as if you were looking from the perspective of the sun"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
if not (camera := bpy.data.objects.get("SunPathCamera")):
|
||||
camera = bpy.data.objects.new("SunPathCamera", bpy.data.cameras.new("SunPathCamera"))
|
||||
assert isinstance(camera.data, bpy.types.Camera)
|
||||
assert context.scene
|
||||
camera.data.type = "ORTHO"
|
||||
camera.data.ortho_scale = 100 # The default of 6m is too small
|
||||
context.scene.collection.objects.link(camera)
|
||||
tool.Blender.activate_camera(camera)
|
||||
props = tool.Blender.get_solar_props()
|
||||
props.hour = props.hour # Just to refresh camera position
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LightPickCoordinates(bpy.types.Operator):
|
||||
bl_idname = "bim.light_pick_coordinates"
|
||||
bl_label = "Pick Coordinates"
|
||||
bl_description = (
|
||||
"Open web browser with Google Maps to pick coordinates (Right Mouse Click in maps to copy selected location).\n\n"
|
||||
"ALT+Click to insert current location based on the current IP-address (using ip-api.com)."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
use_current_location: bool
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.alt:
|
||||
self.use_current_location = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
if not self.use_current_location:
|
||||
zoom = 13.5
|
||||
url = f"https://www.google.com/maps/@{props.latitude},{props.longitude},{zoom}z"
|
||||
webbrowser.open(url)
|
||||
return {"FINISHED"}
|
||||
|
||||
response = requests.get("http://ip-api.com/json/")
|
||||
data = response.json()
|
||||
props.latitude = data["lat"]
|
||||
props.longitude = data["lon"]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LightSetTimeToNow(bpy.types.Operator):
|
||||
bl_idname = "bim.light_set_time_to_now"
|
||||
bl_label = "Now"
|
||||
bl_description = "Set time to current local time."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_solar_props()
|
||||
props.set_from_datetime(datetime.now())
|
||||
return {"FINISHED"}
|
||||
@@ -22,97 +22,52 @@ from typing import TYPE_CHECKING
|
||||
import bpy
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bonsai.bim.module.light.data import SolarData
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Root panel (replaces the old "Radiance Exporter" nested panel)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_PT_radiance_exporter(bpy.types.Panel):
|
||||
"""Creates a Panel in the render properties window"""
|
||||
|
||||
bl_label = "Radiance Exporter"
|
||||
bl_idname = "BIM_PT_radiance_exporter"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_lighting"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Scene Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_PT_radiance_scene_setup(bpy.types.Panel):
|
||||
bl_label = "Scene Setup"
|
||||
bl_idname = "BIM_PT_radiance_scene_setup"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_radiance_exporter"
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
|
||||
if tool.Ifc.get():
|
||||
row = self.layout.row()
|
||||
row.prop(props, "should_load_from_memory")
|
||||
|
||||
if not tool.Ifc.get() or not props.should_load_from_memory:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "ifc_file")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "output_dir")
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "use_active_camera")
|
||||
if not props.use_active_camera:
|
||||
row = layout.row()
|
||||
row.prop(props, "selected_camera")
|
||||
row.operator("radiance.select_camera", text="", icon="EYEDROPPER")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Resolution")
|
||||
row.prop(props, "radiance_resolution_x", text="X")
|
||||
row.prop(props, "radiance_resolution_y", text="Y")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Materials
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_PT_radiance_materials(bpy.types.Panel):
|
||||
bl_label = "Materials"
|
||||
bl_idname = "BIM_PT_radiance_materials"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_radiance_exporter"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
|
||||
layout.label(text="Unmapped materials default to white", icon="INFO")
|
||||
|
||||
layout.label(text="Info: Unmapped materials default to white")
|
||||
row = layout.row()
|
||||
row.template_list("MATERIAL_UL_radiance_materials", "", props, "materials", props, "active_material_index")
|
||||
row.operator("radiance.open_spectraldb", text="", icon="WORLD")
|
||||
|
||||
row.operator("radiance.open_spectraldb", text="", icon="WORLD") # Globe icon
|
||||
if len(props.materials) > 0:
|
||||
col = layout.column(align=True)
|
||||
prop_with_search(col, props, "category")
|
||||
col.prop(props, "category")
|
||||
if props.category:
|
||||
prop_with_search(col, props, "subcategory")
|
||||
col.prop(props, "subcategory")
|
||||
|
||||
if 0 <= props.active_material_index < len(props.materials):
|
||||
if props.active_material_index >= 0 and props.active_material_index < len(props.materials):
|
||||
active_material = props.materials[props.active_material_index]
|
||||
if active_material.category and active_material.subcategory:
|
||||
layout.label(
|
||||
text=f"Mapped: {active_material.name} -> {active_material.category} - {active_material.subcategory}"
|
||||
text=f"Mapped: {active_material.name} to {active_material.category} - {active_material.subcategory}"
|
||||
)
|
||||
else:
|
||||
layout.label(text=f"Select category and subcategory for: {active_material.name}")
|
||||
@@ -123,95 +78,28 @@ class BIM_PT_radiance_materials(bpy.types.Panel):
|
||||
row = layout.row()
|
||||
row.operator("bim.refresh_ifc_materials", text="Refresh IFC Materials")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Lighting (Environment + IES)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_PT_radiance_lighting(bpy.types.Panel):
|
||||
bl_label = "Lighting"
|
||||
bl_idname = "BIM_PT_radiance_lighting"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_radiance_exporter"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
|
||||
# Environment
|
||||
box = layout.box()
|
||||
box.label(text="Environment", icon="WORLD")
|
||||
row = box.row()
|
||||
row.prop(props, "use_hdr")
|
||||
row = box.row()
|
||||
row.prop(props, "use_sun")
|
||||
if props.use_sun:
|
||||
box.prop(props, "sky_condition")
|
||||
row = box.row()
|
||||
row.prop(props, "ground_reflectance")
|
||||
row = box.row()
|
||||
row.prop(props, "turbidity")
|
||||
row = layout.row()
|
||||
layout.label(text="Step 1: Export geometry for simulation")
|
||||
row = layout.row()
|
||||
row.operator("export_scene.radiance", text="Export Geometry for Simulation")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# IES Light Fixtures
|
||||
box = layout.box()
|
||||
box.label(text="IES Light Fixtures", icon="LIGHT_POINT")
|
||||
box.label(text="Camera Settings")
|
||||
row = box.row()
|
||||
row.template_list("MATERIAL_UL_ies_lights", "", props, "ies_lights", props, "active_ies_light_index")
|
||||
col = row.column(align=True)
|
||||
col.operator("radiance.add_ies_light", text="", icon="ADD")
|
||||
row.prop(props, "use_active_camera")
|
||||
if not props.use_active_camera:
|
||||
row = box.row()
|
||||
row.prop(props, "selected_camera")
|
||||
row.operator("radiance.select_camera", text="", icon="EYEDROPPER")
|
||||
|
||||
if len(props.ies_lights) > 0 and 0 <= props.active_ies_light_index < len(props.ies_lights):
|
||||
active_light = props.ies_lights[props.active_ies_light_index]
|
||||
|
||||
col = box.column(align=True)
|
||||
|
||||
row = col.row()
|
||||
row.prop(active_light, "use_collection", text="Target Collection", icon="OUTLINER_COLLECTION")
|
||||
|
||||
row = col.row()
|
||||
if active_light.use_collection:
|
||||
row.prop(active_light, "target_collection", text="Collection")
|
||||
if active_light.target_collection:
|
||||
empties = [o for o in active_light.target_collection.all_objects if o.type == "EMPTY"]
|
||||
row = col.row()
|
||||
row.label(text=f"{len(empties)} empty object(s) in collection", icon="INFO")
|
||||
else:
|
||||
row.prop(active_light, "target_object", text="Object")
|
||||
|
||||
row = col.row()
|
||||
row.label(text="Rotation Z")
|
||||
row.prop(active_light, "rotation_z", text="")
|
||||
|
||||
row = col.row()
|
||||
row.prop(active_light, "lamp_color")
|
||||
|
||||
split = col.split(factor=0.5)
|
||||
split.prop(active_light, "multiply_factor")
|
||||
split.prop(active_light, "radius")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Render Settings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_PT_radiance_render_settings(bpy.types.Panel):
|
||||
bl_label = "Render Settings"
|
||||
bl_idname = "BIM_PT_radiance_render_settings"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_radiance_exporter"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
row = box.row(align=True)
|
||||
row.label(text="Resolution")
|
||||
row.prop(props, "radiance_resolution_x", text="X")
|
||||
row.prop(props, "radiance_resolution_y", text="Y")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "radiance_quality")
|
||||
@@ -222,9 +110,6 @@ class BIM_PT_radiance_render_settings(bpy.types.Panel):
|
||||
row = layout.row()
|
||||
row.prop(props, "radiance_variability")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "ambient_bounces")
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row()
|
||||
@@ -232,94 +117,20 @@ class BIM_PT_radiance_render_settings(bpy.types.Panel):
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "output_file_format")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Pipeline (Steps 1-4 + Cleanup)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_PT_radiance_pipeline(bpy.types.Panel):
|
||||
bl_label = "Pipeline"
|
||||
bl_idname = "BIM_PT_radiance_pipeline"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_radiance_exporter"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
|
||||
# Step 1
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="Step 1: Export Geometry")
|
||||
row = box.row()
|
||||
if props.is_exporting:
|
||||
row.label(text="Exporting...", icon="SORTTIME")
|
||||
else:
|
||||
row.operator("export_scene.radiance", text="Export Geometry")
|
||||
|
||||
# Step 2
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="Step 2: Prepare Scene")
|
||||
row = box.row()
|
||||
if props.is_preparing:
|
||||
row.label(text="Preparing scene...", icon="SORTTIME")
|
||||
else:
|
||||
row.operator("scene.prepare_radiance", text="Prepare Scene")
|
||||
|
||||
# Step 3
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="Step 3: Render")
|
||||
row = box.row()
|
||||
if props.is_rendering:
|
||||
row.label(text="Rendering...", icon="RENDER_STILL")
|
||||
else:
|
||||
row.operator("render_scene.radiance", text="Radiance Render")
|
||||
|
||||
# Step 4: False Color
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="Step 4: False Color Analysis")
|
||||
|
||||
row = box.row()
|
||||
row.prop(props, "radiance_bin_dir")
|
||||
|
||||
row = box.row()
|
||||
row.prop(props, "false_color_label")
|
||||
|
||||
split = box.split(factor=0.5)
|
||||
split.prop(props, "false_color_scale")
|
||||
split.prop(props, "false_color_steps")
|
||||
|
||||
row = box.row()
|
||||
row.label(text="Output Name")
|
||||
row.prop(props, "false_color_output_name", text="")
|
||||
|
||||
row = box.row()
|
||||
row.prop(props, "false_color_contour_lines")
|
||||
|
||||
if props.false_color_contour_lines:
|
||||
row = box.row()
|
||||
row.prop(props, "false_color_contour_mode")
|
||||
|
||||
row = box.row()
|
||||
row.operator("render_scene.false_color_radiance", text="Generate False Color Image")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Cleanup
|
||||
row = layout.row()
|
||||
row.operator("radiance.cleanup_files", text="Cleanup Generated Files", icon="TRASH")
|
||||
row.prop(props, "use_hdr")
|
||||
|
||||
if props.use_hdr:
|
||||
row = layout.row()
|
||||
row.prop(props, "choose_hdr_image")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Solar Panel (unchanged)
|
||||
# ---------------------------------------------------------------------------
|
||||
row = layout.row()
|
||||
layout.label(text="Step 2: Run the simulation")
|
||||
row = layout.row()
|
||||
row.operator("render_scene.radiance", text="Radiance Render")
|
||||
row.enabled = not props.is_exporting
|
||||
|
||||
|
||||
class BIM_PT_solar(bpy.types.Panel):
|
||||
@@ -437,10 +248,7 @@ class BIM_PT_solar(bpy.types.Panel):
|
||||
row = self.layout.row()
|
||||
sun_props = tool.Blender.get_sun_props()
|
||||
assert sun_props
|
||||
if sun_props.sun_object is not None and sun_props.sun_object.data is not None:
|
||||
row.prop(sun_props.sun_object.data, "energy", text="Sun Intensity")
|
||||
else:
|
||||
row.label(text="Sun object not found. Toggle shadow mode to recreate.", icon="ERROR")
|
||||
row.prop(sun_props.sun_object.data, "energy", text="Sun Intensity")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.view_from_sun", icon="LIGHT_HEMI")
|
||||
|
||||
@@ -102,6 +102,7 @@ class MaterialsData:
|
||||
if (style_name := s.Name) is not None
|
||||
]
|
||||
results = natsorted(results, key=lambda i: i[1])
|
||||
results.insert(0, ("-", "No Surface Style", ""))
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -210,15 +210,14 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_material_to_selected"
|
||||
bl_label = "Assign Material To Selected"
|
||||
bl_description = (
|
||||
"Assign currently selected material in Materials UI to the selected objects.\n"
|
||||
"Occurrences automatically get usages for layer/profile sets.\n\n"
|
||||
"ALT+CLICK to assign without a usage."
|
||||
"Assign currently selected material in Materials UI to the selected objects.\n\n"
|
||||
"ALT+CLICK to assign material as a usage."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty(name="Material IFC ID")
|
||||
should_auto_assign_usage: bpy.props.BoolProperty(
|
||||
name="Auto Assign Usage",
|
||||
default=True,
|
||||
assign_as_usage: bpy.props.BoolProperty(
|
||||
name="Assign Material As A Usage",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
@@ -231,19 +230,25 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
self.should_auto_assign_usage = False
|
||||
material_class = tool.Ifc.get().by_id(self.material).is_a()
|
||||
if material_class not in ("IfcMaterialProfileSet", "IfcMaterialLayerSet"):
|
||||
self.report({"ERROR"}, f"{material_class} cannot be assigned as a usage.")
|
||||
return {"CANCELLED"}
|
||||
self.assign_as_usage = True
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
material = tool.Ifc.get().by_id(self.material)
|
||||
objects = tool.Blender.get_selected_objects()
|
||||
material_type = material.is_a()
|
||||
if self.assign_as_usage:
|
||||
material_type += "Usage"
|
||||
core.assign_material(
|
||||
tool.Ifc,
|
||||
tool.Material,
|
||||
material_type=material.is_a(),
|
||||
material_type=material_type,
|
||||
objects=objects,
|
||||
material=material,
|
||||
should_auto_assign_usage=self.should_auto_assign_usage,
|
||||
)
|
||||
|
||||
|
||||
@@ -609,25 +614,29 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
attributes=attributes,
|
||||
)
|
||||
|
||||
layer_sets_to_regenerate = set()
|
||||
slab_planer = slab.DumbSlabPlaner()
|
||||
wall_objs = []
|
||||
|
||||
for obj in objects:
|
||||
obj_element = tool.Ifc.get_entity(obj)
|
||||
obj_material_usage = ifcopenshell.util.element.get_material(obj_element)
|
||||
|
||||
if obj_material_usage and obj_material_usage.is_a("IfcMaterialLayerSetUsage"):
|
||||
obj_material_usage.OffsetFromReferenceLine = material.OffsetFromReferenceLine
|
||||
obj_material_usage.DirectionSense = material.DirectionSense
|
||||
obj_material_usage.ReferenceExtent = material.ReferenceExtent
|
||||
|
||||
layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet)
|
||||
|
||||
# Save custom offset to BBIM_MaterialLayer pset
|
||||
tool.Model.save_custom_offset_to_pset(obj_element, obj)
|
||||
|
||||
for layer_set in layer_sets_to_regenerate:
|
||||
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
|
||||
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
|
||||
# Targeted regeneration: only update this element's geometry, not
|
||||
# all elements sharing the layer set (which would corrupt unrelated instances).
|
||||
if obj_material_usage.LayerSetDirection == "AXIS3":
|
||||
slab_planer.regenerate_from_occurence(obj_element, obj_material_usage)
|
||||
elif obj_material_usage.LayerSetDirection == "AXIS2":
|
||||
wall_objs.append(obj)
|
||||
|
||||
if wall_objs:
|
||||
tool.Model.recalculate_walls(wall_objs)
|
||||
|
||||
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
|
||||
if "CardinalPoint" in attributes:
|
||||
@@ -717,11 +726,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
self.props.material_set_item_material = str(material_set_item.Material.id())
|
||||
|
||||
self.props.material_set_item_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes(
|
||||
material_set_item,
|
||||
self.props.material_set_item_attributes,
|
||||
callback=self.import_attributes_callback,
|
||||
)
|
||||
bonsai.bim.helper.import_attributes(material_set_item, self.props.material_set_item_attributes)
|
||||
|
||||
if material_set_item.is_a("IfcMaterialProfile"):
|
||||
if material_set_item.Profile and material_set_item.Profile.ProfileName:
|
||||
@@ -729,29 +734,6 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def import_attributes_callback(
|
||||
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
|
||||
) -> None | Literal[True]:
|
||||
if data["type"] != "IfcMaterialLayer" or name != "IsVentilated" or not prop:
|
||||
return None
|
||||
|
||||
# Keep null semantics unchanged on export, but avoid an empty UI selection.
|
||||
prop.data_type = "enum"
|
||||
prop.special_type = "LOGICAL"
|
||||
prop.enum_items = json.dumps(("TRUE", "FALSE", "UNKNOWN"))
|
||||
|
||||
value = data[name]
|
||||
if value == "UNKNOWN":
|
||||
prop.enum_value = "UNKNOWN"
|
||||
elif value is None:
|
||||
# Keep visible default as FALSE, but preserve null semantics on save.
|
||||
prop.enum_value = "FALSE"
|
||||
prop.is_null = True
|
||||
else:
|
||||
prop.enum_value = "TRUE" if value else "FALSE"
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DisableEditingMaterialSetItem(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_material_set_item"
|
||||
|
||||
@@ -118,17 +118,12 @@ class BIM_PT_materials(Panel):
|
||||
row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
elif self.props.editing_material_type == "STYLE":
|
||||
if MaterialsData.data["styles"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "contexts", text="")
|
||||
prop_with_search(row, self.props, "styles", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Styles Found")
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "contexts", text="")
|
||||
prop_with_search(row, self.props, "styles", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
|
||||
|
||||
class BIM_PT_object_material(Panel):
|
||||
|
||||
@@ -136,7 +136,7 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"Will unassign element from a type if type has a representation."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
mode: bpy.props.EnumProperty(
|
||||
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
default="BOOLEAN",
|
||||
items=tuple((i, i, "") for i in get_args(SplitAlongEdgeMode)),
|
||||
)
|
||||
@@ -359,7 +359,7 @@ class ConfirmQuickFavoriteOperator(bpy.types.Operator):
|
||||
bl_idname = "bim.confirm_quick_favorite_operator"
|
||||
bl_label = "Confirm Operator"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
index: int
|
||||
@@ -452,8 +452,10 @@ class MoveQuickFavoritesItem(bpy.types.Operator):
|
||||
bl_idname = "bim.move_quick_favorites_item"
|
||||
bl_label = "Move Quick Favorites Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
direction: bpy.props.EnumProperty(items=[("UP", "Up", ""), ("DOWN", "Down", "")])
|
||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
direction: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[("UP", "Up", ""), ("DOWN", "Down", "")]
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
index: int
|
||||
@@ -472,7 +474,7 @@ class RemoveQuickFavoritesItem(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_quick_favorites_item"
|
||||
bl_label = "Remove Quick Favorites Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
index: int
|
||||
|
||||
@@ -36,9 +36,9 @@ QuickFavoriteValueType = Literal["float_value", "bool_value", "int_value", "stri
|
||||
|
||||
|
||||
class QuickFavoriteEnumItem(PropertyGroup):
|
||||
name: StringProperty(name="Name", default="")
|
||||
display_name: StringProperty(name="Display Name", default="")
|
||||
description: StringProperty(name="Description", default="")
|
||||
name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration]
|
||||
display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration]
|
||||
description: StringProperty(name="Description", default="") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
@@ -51,19 +51,19 @@ def get_enum_items(self: "QuickFavoriteProperty", context: bpy.types.Context | N
|
||||
|
||||
|
||||
class QuickFavoriteProperty(PropertyGroup):
|
||||
name: StringProperty(name="Name", default="")
|
||||
display_name: StringProperty(name="Display Name", default="")
|
||||
value_prop: EnumProperty(
|
||||
name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration]
|
||||
display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration]
|
||||
value_prop: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Value Prop",
|
||||
items=tuple((v, v, "") for v in get_args(QuickFavoriteValueType)),
|
||||
)
|
||||
string_value: StringProperty(name="String Value", default="")
|
||||
float_value: FloatProperty(name="Float Value", default=0.0)
|
||||
int_value: IntProperty(name="Int Value", default=0)
|
||||
bool_value: BoolProperty(name="Bool Value", default=False)
|
||||
enum_value: EnumProperty(name="Enum Value", items=get_enum_items)
|
||||
enum_items: CollectionProperty(type=QuickFavoriteEnumItem)
|
||||
is_active: BoolProperty(
|
||||
string_value: StringProperty(name="String Value", default="") # pyright: ignore[reportRedeclaration]
|
||||
float_value: FloatProperty(name="Float Value", default=0.0) # pyright: ignore[reportRedeclaration]
|
||||
int_value: IntProperty(name="Int Value", default=0) # pyright: ignore[reportRedeclaration]
|
||||
bool_value: BoolProperty(name="Bool Value", default=False) # pyright: ignore[reportRedeclaration]
|
||||
enum_value: EnumProperty(name="Enum Value", items=get_enum_items) # pyright: ignore[reportRedeclaration]
|
||||
enum_items: CollectionProperty(type=QuickFavoriteEnumItem) # pyright: ignore[reportRedeclaration]
|
||||
is_active: BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Is Active",
|
||||
description="Only active properties will be added to the operator when invoked from Quick Favorites",
|
||||
default=False,
|
||||
@@ -100,20 +100,20 @@ def get_operator_suggestions(self: "QuickFavoritesItem", context: bpy.types.Cont
|
||||
|
||||
|
||||
class QuickFavoritesItem(PropertyGroup):
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=False)
|
||||
search: StringProperty(
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=False) # pyright: ignore[reportRedeclaration]
|
||||
search: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Search",
|
||||
default="",
|
||||
search=get_operator_suggestions,
|
||||
# Resetting `search_options`, allowing users only to use suggestions.
|
||||
search_options=set(),
|
||||
)
|
||||
properties: CollectionProperty(type=QuickFavoriteProperty)
|
||||
operator_id: StringProperty(
|
||||
properties: CollectionProperty(type=QuickFavoriteProperty) # pyright: ignore[reportRedeclaration]
|
||||
operator_id: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Operator ID",
|
||||
default="",
|
||||
)
|
||||
label: StringProperty(
|
||||
label: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Label",
|
||||
description="Label that will be used in Quick Favorites for this operator",
|
||||
default="",
|
||||
@@ -139,15 +139,15 @@ class QuickFavoritesItem(PropertyGroup):
|
||||
|
||||
|
||||
class BIMMiscProperties(PropertyGroup):
|
||||
total_storeys: IntProperty(
|
||||
total_storeys: IntProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Total Storeys",
|
||||
description="Number of storeys above object's storey to take into account for resizing",
|
||||
default=1,
|
||||
)
|
||||
override_colour: FloatVectorProperty(
|
||||
override_colour: FloatVectorProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Override Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4
|
||||
)
|
||||
quick_favorites: CollectionProperty(type=QuickFavoritesItem)
|
||||
quick_favorites: CollectionProperty(type=QuickFavoritesItem) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
total_storeys: int
|
||||
|
||||
@@ -82,7 +82,7 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
|
||||
|
||||
|
||||
class BIM_OT_add_object(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_grid"
|
||||
bl_idname = "mesh.add_grid"
|
||||
bl_label = "Grid"
|
||||
bl_description = "Add IfcGrid."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@@ -227,7 +227,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
|
||||
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
|
||||
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
|
||||
|
||||
if not all([is_parallel12, is_parallel13, is_parallel21, is_parallel23]):
|
||||
if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23):
|
||||
fitting_type = "WYE"
|
||||
|
||||
if not fitting_type:
|
||||
@@ -903,7 +903,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
||||
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||
radius: bpy.props.FloatProperty(
|
||||
name="Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
|
||||
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -151,11 +151,29 @@ class FilledOpeningGenerator:
|
||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
assert representation
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
else:
|
||||
representation = self.generate_opening_from_filling(
|
||||
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
||||
)
|
||||
|
||||
# Check if mapped representation - PRESERVE the mapping structure
|
||||
if (
|
||||
representation.RepresentationType == "MappedRepresentation"
|
||||
and len(representation.Items) == 1
|
||||
and representation.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
# Store the existing RepresentationMap to reuse it
|
||||
existing_mapping_source = representation.Items[0].MappingSource
|
||||
reuse_mapped_representation = True
|
||||
else:
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
if not reuse_mapped_representation:
|
||||
# Check for library template before generating from filling
|
||||
template_rep = self.get_opening_template_from_type(filling)
|
||||
|
||||
if template_rep:
|
||||
representation = template_rep
|
||||
else:
|
||||
representation = self.generate_opening_from_filling(
|
||||
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
||||
)
|
||||
|
||||
# Create mapped representation
|
||||
if reuse_mapped_representation:
|
||||
@@ -229,38 +247,109 @@ class FilledOpeningGenerator:
|
||||
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
||||
|
||||
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
# ALWAYS preserve the existing opening representation (Tessellation, SweptSolid, etc.)
|
||||
preserved_representation = None
|
||||
if opening_rep:
|
||||
if (
|
||||
opening_rep.RepresentationType == "MappedRepresentation"
|
||||
and len(opening_rep.Items) == 1
|
||||
and opening_rep.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
# For mapped representations, copy the underlying representation
|
||||
preserved_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
opening_rep.Items[0].MappingSource.MappedRepresentation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
)
|
||||
else:
|
||||
# For direct representations (non-mapped), copy them too
|
||||
preserved_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(), opening_rep, exclude=["IfcGeometricRepresentationContext"]
|
||||
)
|
||||
|
||||
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
|
||||
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
|
||||
|
||||
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
|
||||
|
||||
# Priority order for choosing representation:
|
||||
# 1. Existing occurrence with MappedRepresentation (preserve mapping!)
|
||||
# 2. Library template with Tessellation
|
||||
# 3. Preserved representation from old opening (maintain user's work)
|
||||
# 4. Generate from filling (last resort)
|
||||
|
||||
representation_to_use = None
|
||||
reuse_mapped_representation = False
|
||||
existing_mapping_source = None
|
||||
|
||||
if existing_opening_occurrence:
|
||||
representation = ifcopenshell.util.representation.get_representation(
|
||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
tool.Ifc.get(), representation=representation
|
||||
)
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
)
|
||||
else:
|
||||
|
||||
if (
|
||||
representation
|
||||
and representation.RepresentationType == "MappedRepresentation"
|
||||
and len(representation.Items) == 1
|
||||
and representation.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
# PRESERVE the mapped structure - reuse the same RepresentationMap
|
||||
existing_mapping_source = representation.Items[0].MappingSource
|
||||
reuse_mapped_representation = True
|
||||
else:
|
||||
representation_to_use = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
if not representation_to_use and not reuse_mapped_representation:
|
||||
template_rep = self.get_opening_template_from_type(filling)
|
||||
if template_rep and template_rep.RepresentationType == "Tessellation":
|
||||
representation_to_use = template_rep
|
||||
|
||||
if not representation_to_use and not reuse_mapped_representation and preserved_representation:
|
||||
representation_to_use = preserved_representation
|
||||
|
||||
if not representation_to_use and not reuse_mapped_representation:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
if opening_obj:
|
||||
tool.Ifc.unlink(element=opening)
|
||||
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
|
||||
|
||||
filling_obj = tool.Ifc.get_object(filling)
|
||||
representation = self.generate_opening_from_filling(filling, filling_obj)
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
tool.Ifc.get(), representation=representation
|
||||
representation_to_use = self.generate_opening_from_filling(filling, filling_obj)
|
||||
|
||||
# Create the mapped representation
|
||||
if reuse_mapped_representation:
|
||||
# Reuse existing RepresentationMap - don't create a new one!
|
||||
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
new_mapped_item = tool.Ifc.get().create_entity(
|
||||
"IfcMappedItem",
|
||||
MappingSource=existing_mapping_source,
|
||||
MappingTarget=tool.Ifc.get().create_entity(
|
||||
"IfcCartesianTransformationOperator3D",
|
||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
|
||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 1.0, 0.0)),
|
||||
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
|
||||
Scale=1.0,
|
||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
|
||||
),
|
||||
)
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
mapped_representation = tool.Ifc.get().create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
ContextOfItems=context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="MappedRepresentation",
|
||||
Items=[new_mapped_item],
|
||||
)
|
||||
else:
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
tool.Ifc.get(), representation=representation_to_use
|
||||
)
|
||||
|
||||
# update voided object representation or all it's parts if it's an aggregate
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
)
|
||||
|
||||
# update voided object representation...
|
||||
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
|
||||
for voided_element in voided_elements:
|
||||
voided_obj = tool.Ifc.get_object(voided_element)
|
||||
@@ -274,6 +363,36 @@ class FilledOpeningGenerator:
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
def get_opening_template_from_type(
|
||||
self, filling: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""
|
||||
Check if the filling's type has a stored opening template from library import.
|
||||
"""
|
||||
element_type = ifcopenshell.util.element.get_type(filling)
|
||||
|
||||
if not element_type:
|
||||
return None
|
||||
|
||||
desc = element_type.Description
|
||||
|
||||
if not desc or "||BonsaiOpeningTemplate:" not in desc:
|
||||
return None
|
||||
|
||||
# Extract template ID
|
||||
marker = desc.split("||BonsaiOpeningTemplate:")[-1]
|
||||
template_id = int(marker.split("||")[0])
|
||||
|
||||
try:
|
||||
template_rep = tool.Ifc.get().by_id(template_id)
|
||||
# Make a copy so we don't reuse the same representation instance
|
||||
copied = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(), template_rep, exclude=["IfcGeometricRepresentationContext"]
|
||||
)
|
||||
return copied
|
||||
except:
|
||||
return None
|
||||
|
||||
def generate_opening_from_filling(
|
||||
self,
|
||||
filling: ifcopenshell.entity_instance,
|
||||
@@ -540,16 +659,6 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
||||
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
|
||||
|
||||
rep_obj = tool.Geometry.get_geometry_props().representation_obj
|
||||
if booleans:
|
||||
# Users typically select two top-level items and expect the
|
||||
# operand to be absorbed into the boolean, not remain as a
|
||||
# standalone item alongside it.
|
||||
representation = tool.Geometry.get_active_representation(rep_obj)
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
second_items_set = set(second_items)
|
||||
new_items = [i for i in representation.Items if i not in second_items_set]
|
||||
if new_items:
|
||||
representation.Items = new_items
|
||||
rep_element = tool.Ifc.get_entity(rep_obj)
|
||||
tool.Model.mark_manual_booleans(rep_element, booleans)
|
||||
tool.Geometry.reload_representation(rep_obj)
|
||||
|
||||
@@ -421,7 +421,7 @@ class PolylineOperator:
|
||||
tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state)
|
||||
|
||||
tool.Blender.update_viewport()
|
||||
return {"RUNNING_MODAL"}
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
|
||||
props = tool.Model.get_model_props()
|
||||
@@ -461,7 +461,6 @@ class PolylineOperator:
|
||||
self.tool_state.axis_method = None
|
||||
self.tool_state.plane_method = None
|
||||
self.tool_state.mode = "Mouse"
|
||||
tool.Raycast.clear_snap_objs()
|
||||
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
||||
for obj in self.visible_objs:
|
||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||
|
||||
@@ -545,7 +545,7 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_type_page"
|
||||
bl_label = "Change Type Page"
|
||||
bl_options = {"REGISTER"}
|
||||
page: bpy.props.IntProperty()
|
||||
page: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
page: int
|
||||
@@ -694,14 +694,10 @@ def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[
|
||||
|
||||
new_settings = settings.copy()
|
||||
new_settings["context"] = box_context
|
||||
new_box = ifcopenshell.api.geometry.add_representation(
|
||||
ifc_file,
|
||||
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||
**new_settings,
|
||||
)
|
||||
new_box = ifcopenshell.api.geometry.add_representation(ifc_file, should_run_listeners=False, **new_settings)
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
ifc_file,
|
||||
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||
should_run_listeners=False,
|
||||
product=product,
|
||||
representation=new_box,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import copy
|
||||
from math import atan2, degrees, pi, radians
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||
from typing import Any, Literal, Optional, Union
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
@@ -49,7 +49,7 @@ ProfileFrom2PointsReturn = Union[dict[str, Any], None]
|
||||
|
||||
|
||||
class DumbProfileGenerator:
|
||||
def __init__(self, relating_type: ifcopenshell.entity_instance):
|
||||
def __init__(self, relating_type):
|
||||
self.relating_type = relating_type
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
@@ -201,7 +201,7 @@ class DumbProfileGenerator:
|
||||
|
||||
|
||||
class DumbProfileRegenerator:
|
||||
def regenerate_from_profile_def(self, profile: ifcopenshell.entity_instance) -> None:
|
||||
def regenerate_from_profile_def(self, profile):
|
||||
self.file = tool.Ifc.get()
|
||||
objs = []
|
||||
if not profile:
|
||||
@@ -221,7 +221,7 @@ class DumbProfileRegenerator:
|
||||
for element in self.get_element_types_using_profile(profile):
|
||||
tool.Model.mark_thumbnail_for_update(element)
|
||||
|
||||
def regenerate_from_profile(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
|
||||
self.file = ifc_file
|
||||
objs = []
|
||||
profile = settings["profile"].Profile
|
||||
@@ -233,7 +233,7 @@ class DumbProfileRegenerator:
|
||||
objs.append(obj)
|
||||
DumbProfileRecalculator().recalculate(objs)
|
||||
|
||||
def get_elements_using_profile(self, profile: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
def get_elements_using_profile(self, profile):
|
||||
results = []
|
||||
profile_sets = [
|
||||
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
||||
@@ -252,9 +252,7 @@ class DumbProfileRegenerator:
|
||||
results.extend(rel.RelatedObjects)
|
||||
return results
|
||||
|
||||
def get_element_types_using_profile(
|
||||
self, profile: ifcopenshell.entity_instance
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
def get_element_types_using_profile(self, profile):
|
||||
results = []
|
||||
profile_sets = [
|
||||
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
||||
@@ -271,18 +269,12 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.extend_profile"
|
||||
bl_label = "Extend Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
join_type: bpy.props.EnumProperty(
|
||||
items=[("-", "Unjoin", ""), ("L", "L", ""), ("V", "V", ""), ("T", "T", "")],
|
||||
default="-",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
join_type: Literal["-", "L", "V", "T"]
|
||||
join_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = context.selected_objects
|
||||
joiner = DumbProfileJoiner()
|
||||
if self.join_type == "-":
|
||||
if not self.join_type:
|
||||
for obj in selected_objs:
|
||||
joiner.unjoin(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -634,15 +626,11 @@ class DumbProfileJoiner:
|
||||
if connection1 == "ATEND":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
self.body[1] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
|
||||
@@ -685,15 +673,11 @@ class DumbProfileJoiner:
|
||||
elif connection1 == "ATSTART":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
self.body[0] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
|
||||
@@ -737,9 +721,7 @@ class DumbProfileJoiner:
|
||||
if connection1 == "ATEND":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
self.body[1] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(
|
||||
@@ -747,9 +729,7 @@ class DumbProfileJoiner:
|
||||
furthest_plane if is_relating else closest_plane,
|
||||
z_inwards=False if is_relating else True,
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
self.clippings.append(
|
||||
@@ -762,9 +742,7 @@ class DumbProfileJoiner:
|
||||
elif connection1 == "ATSTART":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
self.body[0] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(
|
||||
@@ -772,9 +750,7 @@ class DumbProfileJoiner:
|
||||
furthest_plane if is_relating else closest_plane,
|
||||
z_inwards=False if is_relating else True,
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
self.clippings.append(
|
||||
|
||||
@@ -1729,20 +1729,20 @@ def poll_sverchok_nodes(self: "BIMExternalParametricGeometryProperties", node_tr
|
||||
|
||||
|
||||
class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(
|
||||
is_editing: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Is Editing Paramteric Geometry",
|
||||
description="Toggle editing parametric geometry.",
|
||||
default=False,
|
||||
update=update_is_editing,
|
||||
)
|
||||
geometry_source: bpy.props.EnumProperty(
|
||||
geometry_source: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Geometry Source",
|
||||
items=[
|
||||
("GEONODES", "Geometry Nodes", ""),
|
||||
("IFCSVERCHOK", "IFC Sverchok", ""),
|
||||
],
|
||||
)
|
||||
geo_nodes: bpy.props.PointerProperty(
|
||||
geo_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Geometry Nodes",
|
||||
description="Geometry nodes tree to use as a source for representation.",
|
||||
type=bpy.types.GeometryNodeTree,
|
||||
@@ -1750,7 +1750,7 @@ class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
|
||||
poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"),
|
||||
)
|
||||
|
||||
sverchok_nodes: bpy.props.PointerProperty(
|
||||
sverchok_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Sverchok Nodes",
|
||||
description="Sverchok node tree to use as a source for representation.",
|
||||
type=bpy.types.NodeTree,
|
||||
|
||||
@@ -277,8 +277,19 @@ class DumbSlabPlaner:
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2 * pi, tolerance=0.001) else existing_x_angle
|
||||
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
offset_direction = direction_ratios.copy()
|
||||
perpendicular_depth = thickness * abs(1 / cos(existing_x_angle))
|
||||
perpendicular_offset = layer_offset * abs(1 / cos(existing_x_angle)) / self.unit_scale
|
||||
# The extrusion depth needed to achieve a given perpendicular thickness depends on
|
||||
# how much the extrusion direction deviates from the slab face normal (local Z).
|
||||
# For an ObjectPlacement-rotated slab, extrusion_vec.z ≈ 1.0 → no scaling.
|
||||
# For an ExtrudedDirection-tilted slab, extrusion_vec.z < 1.0 → scale up.
|
||||
extrusion_z = abs(direction_ratios.normalized().z)
|
||||
if extrusion_z > 1e-6:
|
||||
perpendicular_depth = thickness / extrusion_z
|
||||
perpendicular_offset = layer_offset / extrusion_z / self.unit_scale
|
||||
else:
|
||||
perpendicular_depth = thickness
|
||||
perpendicular_offset = layer_offset / self.unit_scale
|
||||
|
||||
ifc_position = extrusion.Position
|
||||
|
||||
# Check angle and z direction to determine whether the extrusion direction is positive or negative
|
||||
if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or (
|
||||
@@ -301,7 +312,6 @@ class DumbSlabPlaner:
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
|
||||
extrusion.Depth = perpendicular_depth
|
||||
|
||||
ifc_position = extrusion.Position
|
||||
position = offset_direction * perpendicular_offset
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material:
|
||||
|
||||
@@ -31,11 +31,11 @@ def calculate_quantities(usecase_path, ifc_file: ifcopenshell.file, settings):
|
||||
return
|
||||
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
|
||||
qto = ifcopenshell.api.pset.add_qto(
|
||||
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities" # ty:ignore[unknown-argument]
|
||||
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities"
|
||||
)
|
||||
ifcopenshell.api.pset.edit_qto(
|
||||
ifc_file,
|
||||
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||
should_run_listeners=False,
|
||||
qto=qto,
|
||||
properties={
|
||||
"StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days,
|
||||
|
||||
@@ -1268,6 +1268,27 @@ class DumbWallJoiner:
|
||||
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
|
||||
return wall2
|
||||
|
||||
def join_Z(self, wall1, slab2):
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(slab2)
|
||||
|
||||
for rel in element1.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
|
||||
ifcopenshell.api.geometry.disconnect_element(
|
||||
tool.Ifc.get(),
|
||||
relating_element=rel.RelatingElement,
|
||||
related_element=element1,
|
||||
)
|
||||
|
||||
ifcopenshell.api.geometry.connect_element(
|
||||
tool.Ifc.get(),
|
||||
relating_element=element2,
|
||||
related_element=element1,
|
||||
description="TOP",
|
||||
)
|
||||
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def set_axis(self, wall, p1, p2):
|
||||
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
@@ -1312,6 +1333,29 @@ class DumbWallJoiner:
|
||||
self.set_axis(element1, p1, p2)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(wall2)
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
axis2 = tool.Model.get_wall_axis(wall2)
|
||||
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
|
||||
if intersect:
|
||||
intersect, _ = intersect
|
||||
else:
|
||||
return
|
||||
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
|
||||
|
||||
ifcopenshell.api.geometry.connect_path(
|
||||
tool.Ifc.get(),
|
||||
related_element=element1,
|
||||
relating_element=element2,
|
||||
relating_connection="ATPATH",
|
||||
related_connection=connection,
|
||||
description="BUTT",
|
||||
)
|
||||
|
||||
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
||||
|
||||
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
|
||||
wall1 = tool.Ifc.get_entity(obj1)
|
||||
wall2 = tool.Ifc.get_entity(obj2)
|
||||
|
||||
@@ -943,7 +943,7 @@ class EditObjectUI:
|
||||
if "LAYER2" in AuthoringData.data["selected_material_usages"]:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_walls_to_underside.__doc__, ui_context
|
||||
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_to_underside.__doc__, ui_context
|
||||
)
|
||||
|
||||
if AuthoringData.data["is_flippable_element"]:
|
||||
|
||||
@@ -33,7 +33,7 @@ class EnableEditingPerson(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_person"
|
||||
bl_label = "Enable Editing Person"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
person: bpy.props.IntProperty()
|
||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
person: int
|
||||
@@ -75,7 +75,7 @@ class RemovePerson(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_person"
|
||||
bl_label = "Remove Person"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
person: bpy.props.IntProperty()
|
||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
person: int
|
||||
@@ -88,7 +88,7 @@ class AddPersonAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.add_person_attribute"
|
||||
bl_label = "Add Person Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.EnumProperty(
|
||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
|
||||
)
|
||||
|
||||
@@ -104,10 +104,10 @@ class RemovePersonAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_person_attribute"
|
||||
bl_label = "Remove Person Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.EnumProperty(
|
||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
|
||||
)
|
||||
id: bpy.props.IntProperty()
|
||||
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
@@ -122,7 +122,7 @@ class EnableEditingRole(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_role"
|
||||
bl_label = "Enable Editing Role"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
role: bpy.props.IntProperty()
|
||||
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
role: int
|
||||
@@ -146,7 +146,7 @@ class AddRole(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_role"
|
||||
bl_label = "Add Role"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.IntProperty()
|
||||
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
parent: int
|
||||
@@ -168,7 +168,7 @@ class RemoveRole(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_role"
|
||||
bl_label = "Remove Role"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
role: bpy.props.IntProperty()
|
||||
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
role: int
|
||||
@@ -181,8 +181,8 @@ class AddAddress(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_address"
|
||||
bl_label = "Add Address"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.IntProperty()
|
||||
ifc_class: bpy.props.EnumProperty(
|
||||
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
ifc_class: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(ADDRESS_TYPE)),
|
||||
)
|
||||
|
||||
@@ -198,7 +198,7 @@ class AddAddressAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.add_address_attribute"
|
||||
bl_label = "Add Address Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.EnumProperty(
|
||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
|
||||
)
|
||||
|
||||
@@ -214,10 +214,10 @@ class RemoveAddressAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_address_attribute"
|
||||
bl_label = "Remove Address Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.EnumProperty(
|
||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
|
||||
)
|
||||
id: bpy.props.IntProperty()
|
||||
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
@@ -232,7 +232,7 @@ class EnableEditingAddress(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_address"
|
||||
bl_label = "Enable Editing Address"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
address: bpy.props.IntProperty()
|
||||
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
address: int
|
||||
@@ -265,7 +265,7 @@ class RemoveAddress(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_address"
|
||||
bl_label = "Remove Address"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
address: bpy.props.IntProperty()
|
||||
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
address: int
|
||||
@@ -278,7 +278,7 @@ class EnableEditingOrganisation(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_organisation"
|
||||
bl_label = "Enable Editing Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
organisation: bpy.props.IntProperty()
|
||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
organisation: int
|
||||
@@ -320,7 +320,7 @@ class RemoveOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_organisation"
|
||||
bl_label = "Remove Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
organisation: bpy.props.IntProperty()
|
||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
organisation: int
|
||||
@@ -333,8 +333,8 @@ class AddPersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_person_and_organisation"
|
||||
bl_label = "Add Person And Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
person: bpy.props.IntProperty()
|
||||
organisation: bpy.props.IntProperty()
|
||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
person: int
|
||||
@@ -350,7 +350,7 @@ class RemovePersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_person_and_organisation"
|
||||
bl_label = "Remove Person And Organisation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
person_and_organisation: bpy.props.IntProperty()
|
||||
person_and_organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
person_and_organisation: int
|
||||
@@ -365,7 +365,7 @@ class SetUser(bpy.types.Operator):
|
||||
bl_idname = "bim.set_user"
|
||||
bl_label = "Set User"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
user: bpy.props.IntProperty()
|
||||
user: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
user: int
|
||||
@@ -401,7 +401,7 @@ class EnableEditingActor(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_actor"
|
||||
bl_label = "Enable Editing Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
actor: bpy.props.IntProperty()
|
||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
actor: int
|
||||
@@ -434,7 +434,7 @@ class RemoveActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_actor"
|
||||
bl_label = "Remove Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
actor: bpy.props.IntProperty()
|
||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
actor: int
|
||||
@@ -447,7 +447,7 @@ class AssignActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_actor"
|
||||
bl_label = "Assign Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
actor: bpy.props.IntProperty()
|
||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
actor: int
|
||||
@@ -462,7 +462,7 @@ class UnassignActor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.unassign_actor"
|
||||
bl_label = "Unassign Actor"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
actor: bpy.props.IntProperty()
|
||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
actor: int
|
||||
@@ -481,7 +481,7 @@ class RemoveApplication(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"Remove provided IfcApplication."
|
||||
"\n\nFor safety will only work on applications without inverses (they are typically marked as '(unused)'."
|
||||
)
|
||||
application_id: bpy.props.IntProperty()
|
||||
application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
application_id: int
|
||||
@@ -525,7 +525,7 @@ class EnableEditingApplication(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_application"
|
||||
bl_label = "Enable Editing Application"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
application_id: bpy.props.IntProperty()
|
||||
application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
application_id: int
|
||||
|
||||
@@ -76,7 +76,6 @@ classes = (
|
||||
operator.UnlinkIfc,
|
||||
operator.UnloadLink,
|
||||
workspace.ExploreHotkey,
|
||||
operator.GenerateUVMap,
|
||||
prop.LibraryBreadcrumb,
|
||||
prop.LibraryElement,
|
||||
prop.FilterCategory,
|
||||
|
||||
@@ -86,7 +86,9 @@ class NewProject(bpy.types.Operator):
|
||||
bl_label = "New Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Start a new IFC project in a fresh session"
|
||||
preset: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(PresetType)])
|
||||
preset: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[(i, i, "") for i in get_args(PresetType)]
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
preset: PresetType
|
||||
@@ -180,11 +182,6 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
append_all: bpy.props.BoolProperty(default=False)
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filter_glob: str
|
||||
append_all: bool
|
||||
use_relative_path: bool
|
||||
|
||||
reload_previous_file = False
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -561,12 +558,8 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.append_library_element_by_query"
|
||||
bl_label = "Append Library Element By Query"
|
||||
|
||||
query: bpy.props.StringProperty(name="Query")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
query: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
@@ -598,11 +591,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
prop_index: bpy.props.IntProperty()
|
||||
assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
definition: int
|
||||
prop_index: int
|
||||
assume_unique_by_name: bool
|
||||
|
||||
file: ifcopenshell.file
|
||||
|
||||
@classmethod
|
||||
@@ -630,6 +618,8 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not element:
|
||||
return {"FINISHED"}
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
# Store opening template from library if it exists
|
||||
self.store_opening_template_from_library(element, library_file)
|
||||
self.import_type_from_ifc(element, context)
|
||||
elif element.is_a("IfcProduct"):
|
||||
# NOTE: Non-types are not exposed in UI directly
|
||||
@@ -730,6 +720,53 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
|
||||
ifc_importer.create_style(element)
|
||||
|
||||
def store_opening_template_from_library(
|
||||
self, element: ifcopenshell.entity_instance, library_file: ifcopenshell.file
|
||||
) -> None:
|
||||
"""
|
||||
Find an opening representation in the library and copy it to the current file
|
||||
as a template. Store the template ID on the type for later retrieval.
|
||||
"""
|
||||
try:
|
||||
library_element = library_file.by_guid(element.GlobalId)
|
||||
except:
|
||||
return
|
||||
|
||||
# Find occurrences with openings in the library
|
||||
library_occurrences = ifcopenshell.util.element.get_types(library_element)
|
||||
|
||||
for occurrence in library_occurrences:
|
||||
if not getattr(occurrence, "FillsVoids", None):
|
||||
continue
|
||||
|
||||
library_opening = occurrence.FillsVoids[0].RelatingOpeningElement
|
||||
library_opening_rep = ifcopenshell.util.representation.get_representation(
|
||||
library_opening, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
|
||||
if not library_opening_rep:
|
||||
continue
|
||||
|
||||
# Check if mapped representation
|
||||
if (
|
||||
library_opening_rep.RepresentationType == "MappedRepresentation"
|
||||
and len(library_opening_rep.Items) == 1
|
||||
and library_opening_rep.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
|
||||
mapped_rep = library_opening_rep.Items[0].MappingSource.MappedRepresentation
|
||||
|
||||
# Store ALL representation types (Tessellation, SweptSolid, etc.)
|
||||
template_rep = ifcopenshell.util.element.copy_deep(
|
||||
self.file, mapped_rep, exclude=["IfcGeometricRepresentationContext"]
|
||||
)
|
||||
|
||||
# Store reference in type's Description
|
||||
current_desc = element.Description or ""
|
||||
element.Description = f"{current_desc}||BonsaiOpeningTemplate:{template_rep.id()}"
|
||||
return
|
||||
break
|
||||
|
||||
|
||||
class EditProjectLibrary(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_project_library"
|
||||
@@ -979,15 +1016,6 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filepath: str
|
||||
filter_glob: str
|
||||
is_advanced: bool
|
||||
use_relative_path: bool
|
||||
should_start_fresh_session: bool
|
||||
import_without_ifc_data: bool
|
||||
use_detailed_tooltip: bool
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
tooltip = cls.bl_description
|
||||
@@ -1088,7 +1116,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
else:
|
||||
return self.finish_loading_project(context)
|
||||
|
||||
def finish_loading_project(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
def finish_loading_project(self, context):
|
||||
try:
|
||||
filepath = self.get_filepath()
|
||||
if not self.is_existing_ifc_file():
|
||||
@@ -1288,9 +1316,6 @@ class ToggleFilterCategories(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
should_select: bpy.props.BoolProperty(name="Should Select", default=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
should_select: bool
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
for filter_category in props.filter_categories:
|
||||
@@ -1313,14 +1338,6 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
default=False,
|
||||
)
|
||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
||||
query: bpy.props.StringProperty(
|
||||
name="Query",
|
||||
description=(
|
||||
"Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n"
|
||||
"Default query - IfcElement, but excluding IfcProxy, IfcSpatialStructureElement, IfcSpatialElement, IfcFeatureElement."
|
||||
),
|
||||
)
|
||||
|
||||
filename_ext = ".ifc"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -1330,25 +1347,20 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
filter_glob: str
|
||||
use_relative_path: bool
|
||||
use_cache: bool
|
||||
query: str
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
pprops = tool.Project.get_project_props()
|
||||
row = self.layout.row()
|
||||
row.prop(self, "use_relative_path")
|
||||
row = self.layout.row()
|
||||
row.prop(self, "use_cache")
|
||||
row = self.layout.row()
|
||||
row.label(text="False Origin Mode:")
|
||||
row = self.layout.row()
|
||||
row.prop(pprops, "false_origin_mode", text="")
|
||||
row.prop(pprops, "false_origin_mode")
|
||||
if pprops.false_origin_mode == "MANUAL":
|
||||
row = self.layout.row()
|
||||
row.prop(pprops, "false_origin")
|
||||
row = self.layout.row()
|
||||
row.prop(pprops, "project_north")
|
||||
self.layout.prop(self, "query", placeholder="IfcElement")
|
||||
|
||||
def _execute(self, context):
|
||||
start = time.time()
|
||||
@@ -1381,7 +1393,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.name = filepath
|
||||
new.filepath = filepath
|
||||
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache, query=self.query)
|
||||
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache)
|
||||
|
||||
|
||||
class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -1389,12 +1401,8 @@ class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Unlink IFC"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Remove the selected file from the link list"
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
link = props.links[self.link_index]
|
||||
@@ -1413,12 +1421,8 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Unload Link"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Unload the selected linked file"
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def _execute(self, context):
|
||||
link = tool.Project.get_project_props().links[self.link_index]
|
||||
if obj := tool.Project.get_link_empty_handle(link):
|
||||
@@ -1440,14 +1444,12 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Load the selected file"
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
||||
query: bpy.props.StringProperty()
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
use_cache: bool
|
||||
query: str
|
||||
|
||||
def _execute(self, context):
|
||||
self.link = tool.Project.get_project_props().links[self.link_index]
|
||||
@@ -1489,20 +1491,8 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def link_ifc(self) -> Union[set[str], None]:
|
||||
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
|
||||
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
|
||||
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
|
||||
|
||||
def should_clear_cache() -> bool:
|
||||
if not self.use_cache:
|
||||
return True
|
||||
if not blend_filepath.exists():
|
||||
return False
|
||||
data = json.loads(json_filepath.read_text())
|
||||
# Empty 'query' - model loaded without custom query.
|
||||
# Missing 'query' - model was loaded before custom queries were introduced in Bonsai.
|
||||
query = data.get("query", "")
|
||||
return query != self.query
|
||||
|
||||
if should_clear_cache():
|
||||
if not self.use_cache and blend_filepath.exists():
|
||||
os.remove(blend_filepath)
|
||||
|
||||
if not blend_filepath.exists():
|
||||
@@ -1530,7 +1520,7 @@ def run():
|
||||
pprops.project_north = "{pprops.project_north}"
|
||||
# Use absolute path to be safe from cwd changes.
|
||||
try:
|
||||
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}", query={repr(self.query)})
|
||||
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}")
|
||||
except RuntimeError as e:
|
||||
# Operator failed (returned CANCELLED with error report)
|
||||
print(f"Failed to load linked project: {{e}}")
|
||||
@@ -1616,12 +1606,8 @@ class ReloadLink(bpy.types.Operator):
|
||||
bl_label = "Reload Link"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Reload the selected file"
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.bim.unload_link(link_index=self.link_index)
|
||||
return bpy.ops.bim.load_link(link_index=self.link_index, use_cache=False) or {"FINISHED"}
|
||||
@@ -1632,12 +1618,8 @@ class ToggleLinkSelectability(bpy.types.Operator):
|
||||
bl_label = "Toggle Link Selectability"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Toggle selectability"
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
link = props.links[self.link_index]
|
||||
@@ -1665,8 +1647,8 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Toggle visibility between SOLID and WIREFRAME"
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
mode: bpy.props.EnumProperty(
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Visibility Mode",
|
||||
items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")),
|
||||
)
|
||||
@@ -1806,12 +1788,8 @@ class SelectLinkHandle(bpy.types.Operator):
|
||||
bl_label = "Select Link Handle"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select link empty object handle"
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
link = props.links[self.link_index]
|
||||
@@ -1829,7 +1807,7 @@ class SelectLinkedModelElement(bpy.types.Operator):
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Select an element in the currently selected linked model by providing GlobalId."
|
||||
|
||||
guid: bpy.props.StringProperty(name="GlobalId")
|
||||
guid: bpy.props.StringProperty(name="GlobalId") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
guid: str
|
||||
@@ -1874,13 +1852,6 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filter_glob: str
|
||||
json_version: str
|
||||
json_compact: bool
|
||||
should_save_as: bool
|
||||
use_relative_path: bool
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
@@ -2029,12 +2000,6 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
query: bpy.props.StringProperty()
|
||||
"""See ``bim.link_ifc``."""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
query: str
|
||||
|
||||
file: ifcopenshell.file
|
||||
meshes: dict[str, bpy.types.Mesh]
|
||||
# Material names is derived from diffuse as in 'r-g-b-a'.
|
||||
@@ -2084,17 +2049,14 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
tool.Loader.settings.context_settings = tool.Loader.create_settings()
|
||||
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
||||
|
||||
if self.query:
|
||||
self.elements = ifcopenshell.util.selector.filter_elements(self.file, self.query)
|
||||
self.elements = set(self.file.by_type("IfcElement"))
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements |= set(self.file.by_type("IfcProxy"))
|
||||
if self.file.schema == "IFC2X3":
|
||||
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
|
||||
else:
|
||||
self.elements = set(self.file.by_type("IfcElement"))
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements |= set(self.file.by_type("IfcProxy"))
|
||||
if self.file.schema == "IFC2X3":
|
||||
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
|
||||
else:
|
||||
self.elements |= set(self.file.by_type("IfcSpatialElement"))
|
||||
self.elements -= set(self.file.by_type("IfcFeatureElement"))
|
||||
self.elements |= set(self.file.by_type("IfcSpatialElement"))
|
||||
self.elements -= set(self.file.by_type("IfcFeatureElement"))
|
||||
|
||||
if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin:
|
||||
tool.Loader.set_manual_blender_offset(self.file)
|
||||
@@ -2119,7 +2081,6 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
"false_origin_mode": pprops.false_origin_mode,
|
||||
"false_origin": pprops.false_origin,
|
||||
"project_north": pprops.project_north,
|
||||
"query": self.query,
|
||||
}
|
||||
with open(self.json_filepath, "w") as f:
|
||||
json.dump(data, f)
|
||||
@@ -2419,8 +2380,8 @@ class HideQueriedLinkedElement(bpy.types.Operator):
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"})
|
||||
hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"})
|
||||
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
||||
hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
unhide_all: bool
|
||||
@@ -2534,7 +2495,7 @@ class EnableCulling(bpy.types.Operator):
|
||||
self.total_mousemoves = 0
|
||||
self.cullable_objects = []
|
||||
|
||||
def modal(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
|
||||
def modal(self, context, event):
|
||||
if not LinksData.enable_culling:
|
||||
for obj in bpy.context.visible_objects:
|
||||
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
||||
@@ -2565,7 +2526,7 @@ class EnableCulling(bpy.types.Operator):
|
||||
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def is_view_changed(self, context: bpy.types.Context) -> bool:
|
||||
def is_view_changed(self, context):
|
||||
view_matrix = context.region_data.view_matrix
|
||||
projection_matrix = context.region_data.window_matrix
|
||||
vp_matrix = projection_matrix @ view_matrix
|
||||
@@ -2580,7 +2541,7 @@ class EnableCulling(bpy.types.Operator):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_object_in_view(self, obj: bpy.types.Object, context: bpy.types.Context, camera_position: Vector) -> bool:
|
||||
def is_object_in_view(self, obj, context, camera_position):
|
||||
# Get the view matrix and the projection matrix from the active viewport
|
||||
view_matrix = context.region_data.view_matrix
|
||||
projection_matrix = context.region_data.window_matrix
|
||||
@@ -2607,7 +2568,7 @@ class EnableCulling(bpy.types.Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set["rna_enums.OperatorReturnItems"]:
|
||||
def invoke(self, context, event):
|
||||
LinksData.enable_culling = True
|
||||
self.cullable_objects = []
|
||||
for obj in bpy.context.visible_objects:
|
||||
@@ -2897,10 +2858,6 @@ class IFCFileHandlerOperator(bpy.types.Operator):
|
||||
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
|
||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
directory: str
|
||||
files: list[bpy.types.OperatorFileListElement]
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Keeping code in .invoke() as we'll probably add some
|
||||
# popup windows later.
|
||||
@@ -2952,9 +2909,6 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
|
||||
|
||||
measure_type: bpy.props.StringProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
measure_type: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.type == "VIEW_3D"
|
||||
@@ -3051,9 +3005,6 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
|
||||
|
||||
measure_type: bpy.props.StringProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
measure_type: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.type == "VIEW_3D"
|
||||
@@ -3156,10 +3107,7 @@ class ClearMeasurement(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
if len(polyline_props.measurement_polyline) > 0:
|
||||
return True
|
||||
cls.poll_message_set("No measurement to clear.")
|
||||
return False
|
||||
return len(polyline_props.measurement_polyline) > 0
|
||||
|
||||
def execute(self, context):
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
@@ -3263,7 +3211,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
||||
super().invoke(context, event)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def cancel_tool(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
def cancel_tool(self, context):
|
||||
context.workspace.status_text_set(text=None)
|
||||
if hasattr(self, "tool_state"):
|
||||
self.tool_state.plane_method = None
|
||||
@@ -3271,7 +3219,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
||||
tool.Blender.update_viewport()
|
||||
return {"CANCELLED"}
|
||||
|
||||
def handle_custom_instructions(self, context: bpy.types.Context) -> None:
|
||||
def handle_custom_instructions(self, context):
|
||||
if len(self.selected_points) == 0:
|
||||
instruction_text = "Click First Point on Image"
|
||||
elif len(self.selected_points) == 1:
|
||||
@@ -3286,14 +3234,14 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
||||
|
||||
context.workspace.status_text_set(text=instruction_text)
|
||||
|
||||
def calculate_distance(self) -> None:
|
||||
def calculate_distance(self):
|
||||
if len(self.selected_points) == 2:
|
||||
point1 = self.selected_points[0]
|
||||
point2 = self.selected_points[1]
|
||||
distance_3d = (point2 - point1).length
|
||||
self.calculated_distance = distance_3d / self.unit_scale
|
||||
|
||||
def apply_scaling(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
def apply_scaling(self, context):
|
||||
if len(self.selected_points) != 2:
|
||||
self.report({"ERROR"}, "Two points must be selected")
|
||||
return {"CANCELLED"}
|
||||
@@ -3353,9 +3301,6 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(name="IFC File Path", default="")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filepath: str
|
||||
|
||||
def execute(self, context):
|
||||
ifc_file = self.filepath
|
||||
if not ifc_file:
|
||||
@@ -3388,19 +3333,3 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
||||
bpy.app.handlers.load_post.append(load_handler)
|
||||
bpy.ops.wm.open_mainfile(filepath=metadata_path)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GenerateUVMap(bpy.types.Operator):
|
||||
bl_idname = "bim.generate_uv_map"
|
||||
bl_label = "Generate UV Map"
|
||||
bl_description = "Generate UV map for selected mesh."
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj or not isinstance(obj.data, bpy.types.Mesh):
|
||||
self.report({"ERROR"}, "No valid mesh selected.")
|
||||
return {"CANCELLED"}
|
||||
tool.Loader.load_generated_uv_map(obj.data)
|
||||
self.report({"INFO"}, "Generated UV map for selected mesh.")
|
||||
return {"FINISHED"}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user