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 |
@@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env -S uv run
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "PyGithub",
|
||||
# "requests",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from github import Github
|
||||
from github.GitReleaseAsset import GitReleaseAsset
|
||||
|
||||
EXTENSION_ID = "bonsai"
|
||||
CURRENT_PYTHON_VERSION = "py313"
|
||||
CURRENT_PLATFORMS = ["linux-x64", "macos-arm64", "windows-x64"]
|
||||
|
||||
|
||||
def publish_asset(asset: GitReleaseAsset, token: str, repo_root: Path) -> None:
|
||||
"""
|
||||
Publish an asset to Blender Extensions.
|
||||
Reference: https://extensions.blender.org/api/v1/swagger
|
||||
"""
|
||||
temp_path = repo_root / asset.name
|
||||
|
||||
response = requests.get(asset.browser_download_url)
|
||||
response.raise_for_status()
|
||||
temp_path.write_bytes(response.content)
|
||||
|
||||
url = f"https://extensions.blender.org/api/v1/extensions/{EXTENSION_ID}/versions/upload/"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
files = {"version_file": temp_path.read_bytes()}
|
||||
response = requests.post(url, headers=headers, files=files)
|
||||
response.raise_for_status()
|
||||
|
||||
temp_path.unlink()
|
||||
|
||||
print(f"✓ Published {asset.name}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
token = os.getenv("BLENDER_EXTENSIONS_TOKEN")
|
||||
if not token:
|
||||
raise Exception("BLENDER_EXTENSIONS_TOKEN environment variable not set")
|
||||
|
||||
# Get the repository root
|
||||
repo_root = Path(__file__).parent.parent.parent
|
||||
|
||||
# Read VERSION file
|
||||
version_file = repo_root / "VERSION"
|
||||
version = version_file.read_text().strip()
|
||||
|
||||
print(f"Current VERSION: {version}")
|
||||
|
||||
tag_name = f"bonsai-{version}"
|
||||
|
||||
# Get release from GitHub
|
||||
gh = Github()
|
||||
gh_repo = gh.get_repo("IfcOpenShell/IfcOpenShell")
|
||||
release = gh_repo.get_release(tag_name)
|
||||
|
||||
assets = release.get_assets()
|
||||
|
||||
asset_platform_map: dict[str, tuple[GitReleaseAsset, str]] = {}
|
||||
for asset in assets:
|
||||
if CURRENT_PYTHON_VERSION not in asset.name:
|
||||
continue
|
||||
for platform in CURRENT_PLATFORMS:
|
||||
if platform in asset.name:
|
||||
asset_platform_map[asset.name] = (asset, platform)
|
||||
break
|
||||
|
||||
if len(asset_platform_map) != len(CURRENT_PLATFORMS):
|
||||
found_platforms = {platform for _, (_, platform) in asset_platform_map.items()}
|
||||
missing_platforms = set(CURRENT_PLATFORMS) - found_platforms
|
||||
raise Exception(
|
||||
f"Expected {len(CURRENT_PLATFORMS)} assets but found {len(asset_platform_map)}. "
|
||||
f"Missing: {', '.join(sorted(missing_platforms))}"
|
||||
)
|
||||
|
||||
print("\nRelease assets:")
|
||||
for asset_name in sorted(asset_platform_map.keys()):
|
||||
print(f"- {asset_name}")
|
||||
|
||||
# https://extensions.blender.org/api/v1/swagger
|
||||
print("\nPublishing assets to Blender Extensions:")
|
||||
for asset_name, (asset, platform) in asset_platform_map.items():
|
||||
publish_asset(asset, token, repo_root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
python ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
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.23
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
|
||||
@@ -9,13 +9,6 @@ jobs:
|
||||
container: rockylinux:9
|
||||
|
||||
steps:
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install Python
|
||||
# Installs latest Python version so it's preferred by uv over Rocky's system Python.
|
||||
run: uv python install
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
dnf update -y
|
||||
@@ -24,6 +17,7 @@ jobs:
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils xz byacc
|
||||
python3 -m pip install typing_extensions
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
- name: Install aws cli
|
||||
@@ -51,10 +45,10 @@ jobs:
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
python3 ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
|
||||
@@ -62,7 +56,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
@@ -77,7 +71,7 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
python3 ../nix/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
|
||||
@@ -9,13 +9,6 @@ jobs:
|
||||
container: arm64v8/rockylinux:9
|
||||
|
||||
steps:
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install Python
|
||||
# Installs latest Python version so it's preferred by uv over Rocky's system Python.
|
||||
run: uv python install
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
dnf update -y
|
||||
@@ -24,6 +17,7 @@ jobs:
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils xz byacc
|
||||
python3 -m pip install typing_extensions
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
- name: Install aws cli
|
||||
@@ -51,10 +45,10 @@ jobs:
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
python3 ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
|
||||
@@ -62,7 +56,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
@@ -77,7 +71,7 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
python3 ../nix/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
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,17 +14,19 @@ 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: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
cat requirements-tools.txt | xargs -L1 uv tool install
|
||||
uv tool install ruff
|
||||
uv tool install black
|
||||
uv tool install poethepoet
|
||||
|
||||
# black doesn't catch all syntax errors, so we check them explicitly.
|
||||
- name: Check syntax errors
|
||||
@@ -36,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
|
||||
|
||||
@@ -55,19 +54,6 @@ jobs:
|
||||
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
|
||||
continue-on-error: true
|
||||
|
||||
- name: ty check (venv setup)
|
||||
run: poe ty-venv
|
||||
|
||||
- name: ty check (bonsai)
|
||||
id: ty-bonsai
|
||||
run: poe ty-bonsai
|
||||
continue-on-error: true
|
||||
|
||||
- name: ty check (ios)
|
||||
id: ty-ios
|
||||
run: poe ty-ios
|
||||
continue-on-error: true
|
||||
|
||||
- name: Ruff check
|
||||
id: ruff
|
||||
run: |
|
||||
@@ -98,7 +84,8 @@ jobs:
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
}
|
||||
|
||||
run_check poe ruff
|
||||
run_check poe ruff-main
|
||||
run_check poe ruff-old
|
||||
|
||||
exit $ERROR
|
||||
continue-on-error: true
|
||||
@@ -115,10 +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-bonsai.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check (bonsai) failed, see 'ty check (bonsai)' step for the details." && ERROR=1
|
||||
fi
|
||||
if [ "${{ steps.ty-ios.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check (ios) failed, see 'ty check (ios)' 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.2/blender-5.2.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
|
||||
@@ -179,7 +179,8 @@ jobs:
|
||||
blender --online-mode --command extension install --enable --sync sun_position
|
||||
|
||||
cd IfcOpenShell/src/bonsai
|
||||
pip install -r requirements-dev.txt
|
||||
pip install pytest-blender
|
||||
pip install pytest-bdd
|
||||
blender --background --python scripts/setup_pytest.py
|
||||
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
|
||||
make test
|
||||
|
||||
@@ -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.23
|
||||
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,155 +0,0 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
name: ci-ifcwrap-standalone
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/ci-ifcwrap-standalone.yml"
|
||||
- "cmake/**"
|
||||
- "src/ifcwrap/**"
|
||||
- "src/ifcparse/**"
|
||||
- "src/ifcgeom/**"
|
||||
- "src/serializers/**"
|
||||
- "src/ifcconvert/**"
|
||||
- "src/ifcopenshell-python/**"
|
||||
- "src/svgfill/**"
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/ci-ifcwrap-standalone.yml"
|
||||
- "cmake/**"
|
||||
- "src/ifcwrap/**"
|
||||
- "src/ifcparse/**"
|
||||
- "src/ifcgeom/**"
|
||||
- "src/serializers/**"
|
||||
- "src/ifcconvert/**"
|
||||
- "src/ifcopenshell-python/**"
|
||||
- "src/svgfill/**"
|
||||
|
||||
env:
|
||||
IFCOPENSHELL_PREFIX: ${{ github.workspace }}/ifcopenshell-install
|
||||
|
||||
jobs:
|
||||
build-ifcopenshell:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install C++ dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt-get install --no-install-recommends -y \
|
||||
cmake \
|
||||
gcc \
|
||||
g++ \
|
||||
libboost-date-time-dev \
|
||||
libboost-filesystem-dev \
|
||||
libboost-iostreams-dev \
|
||||
libboost-program-options-dev \
|
||||
libboost-regex-dev \
|
||||
libboost-system-dev \
|
||||
libboost-thread-dev \
|
||||
libeigen3-dev \
|
||||
libocct-data-exchange-dev \
|
||||
libocct-draw-dev \
|
||||
libocct-foundation-dev \
|
||||
libocct-modeling-algorithms-dev \
|
||||
libocct-modeling-data-dev \
|
||||
libocct-ocaf-dev \
|
||||
libocct-visualization-dev \
|
||||
libpcre3-dev \
|
||||
libtbb-dev \
|
||||
libxml2-dev \
|
||||
libxi-dev \
|
||||
occt-misc \
|
||||
tcl-dev \
|
||||
tk-dev \
|
||||
swig
|
||||
|
||||
- name: Configure minimal IfcOpenShell
|
||||
run: |
|
||||
cmake -S cmake -B build-ifcopenshell \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="${IFCOPENSHELL_PREFIX}" \
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
|
||||
-DMINIMAL_BUILD=ON \
|
||||
-DBUILD_IFCPYTHON=OFF \
|
||||
"-DSCHEMA_VERSIONS=4x3_add2"
|
||||
|
||||
- name: Build and install minimal IfcOpenShell
|
||||
run: |
|
||||
cmake --build build-ifcopenshell --target install -j "$(nproc)"
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install Python import dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install numpy typing_extensions
|
||||
|
||||
- name: Configure standalone IfcPython
|
||||
run: |
|
||||
PYTHON_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')"
|
||||
PYTHON_INCLUDE_DIR="$(python -c 'import sysconfig; print(sysconfig.get_path("include"))')"
|
||||
|
||||
cmake -S src/ifcwrap -B "build-ifcwrap-311" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH="${IFCOPENSHELL_PREFIX};/usr" \
|
||||
-DPython_EXECUTABLE:FILEPATH="${PYTHON_EXECUTABLE}" \
|
||||
-DPython_INCLUDE_DIR:PATH="${PYTHON_INCLUDE_DIR}" \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH="${PYTHON_EXECUTABLE}" \
|
||||
-DPYTHON_INCLUDE_DIR:PATH="${PYTHON_INCLUDE_DIR}"
|
||||
|
||||
- name: Build and install standalone IfcPython
|
||||
run: |
|
||||
cmake --build "build-ifcwrap-311" --target install -j "$(nproc)"
|
||||
|
||||
- name: Import installed IfcPython
|
||||
run: |
|
||||
PYTHONPATH="${RUNNER_TEMP}/ifcopenshell-python" python - <<'PY'
|
||||
import ifcopenshell
|
||||
|
||||
print("IfcOpenShell import ok:", ifcopenshell.version)
|
||||
PY
|
||||
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install Python import dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install numpy typing_extensions
|
||||
|
||||
- name: Configure standalone IfcPython
|
||||
run: |
|
||||
PYTHON_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')"
|
||||
PYTHON_INCLUDE_DIR="$(python -c 'import sysconfig; print(sysconfig.get_path("include"))')"
|
||||
|
||||
cmake -S src/ifcwrap -B "build-ifcwrap-312" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH="${IFCOPENSHELL_PREFIX};/usr" \
|
||||
-DPython_EXECUTABLE:FILEPATH="${PYTHON_EXECUTABLE}" \
|
||||
-DPython_INCLUDE_DIR:PATH="${PYTHON_INCLUDE_DIR}" \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH="${PYTHON_EXECUTABLE}" \
|
||||
-DPYTHON_INCLUDE_DIR:PATH="${PYTHON_INCLUDE_DIR}"
|
||||
|
||||
- name: Build and install standalone IfcPython
|
||||
run: |
|
||||
cmake --build "build-ifcwrap-312" --target install -j "$(nproc)"
|
||||
|
||||
- name: Import installed IfcPython
|
||||
run: |
|
||||
PYTHONPATH="${RUNNER_TEMP}/ifcopenshell-python" python - <<'PY'
|
||||
import ifcopenshell
|
||||
|
||||
print("IfcOpenShell import ok:", ifcopenshell.version)
|
||||
PY
|
||||
@@ -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}"
|
||||
@@ -10,13 +10,9 @@ on:
|
||||
- 'src/ifcgeomserver/**'
|
||||
- 'src/ifcjni/**'
|
||||
- 'src/ifcmax/**'
|
||||
- 'src/ifc5d/**'
|
||||
- 'src/ifcedit/**'
|
||||
- 'src/ifcmcp/**'
|
||||
- 'src/ifcopenshell-python/**'
|
||||
- '!src/ifcopenshell-python/docs/**'
|
||||
- 'src/ifcparse/**'
|
||||
- 'src/ifcquery/**'
|
||||
- 'src/ifcwrap/**'
|
||||
- 'src/qtviewer/**'
|
||||
- 'src/svgfill/**'
|
||||
@@ -55,7 +51,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pyparsing psutil
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
|
||||
pip install src/bcf --no-deps
|
||||
pip install pytest-xdist==3.8.0
|
||||
|
||||
@@ -83,7 +79,7 @@ jobs:
|
||||
libhdf5-dev libcgal-dev libeigen3-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
@@ -256,26 +252,12 @@ jobs:
|
||||
pip install deepdiff
|
||||
cd ../ifcdiff && make test || ERROR=1
|
||||
cd ../ifcpatch && make test || ERROR=1
|
||||
pip install -e ../ifc5d --no-deps
|
||||
pip install odfpy xlsxwriter
|
||||
cd ../ifc5d && make test || ERROR=1
|
||||
pip install -e ../ifcquery --no-deps
|
||||
cd ../ifcquery && make test || ERROR=1
|
||||
pip install -e ../ifcedit --no-deps
|
||||
cd ../ifcedit && make test || ERROR=1
|
||||
pip install mcp
|
||||
pip install -e ../ifcmcp --no-deps
|
||||
cd ../ifcmcp && 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.
|
||||
# mathutils only has pre-built wheels for Python 3.13+; skip on older versions.
|
||||
cd ../ifcopenshell-python
|
||||
if python -c "import sys; sys.exit(0 if sys.version_info >= (3, 13) else 1)"; then
|
||||
pip install mathutils
|
||||
make test-mathutils || ERROR=1
|
||||
fi
|
||||
pip install mathutils
|
||||
make test-mathutils || ERROR=1
|
||||
if [ $ERROR -ne 0 ]; then
|
||||
echo "One or more tests failed";
|
||||
exit 1;
|
||||
|
||||
@@ -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,16 +0,0 @@
|
||||
name: Publish Bonsai Releases
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
|
||||
- run: uv run .github/scripts/publish-bonsai-releases.py
|
||||
env:
|
||||
BLENDER_EXTENSIONS_TOKEN: ${{ secrets.BLENDER_EXTENSIONS_TOKEN }}
|
||||
@@ -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
-22
@@ -4,11 +4,7 @@
|
||||
/_deps-vs*-x*-installed/
|
||||
/_installed-vs*-x*/
|
||||
/build/
|
||||
/build.log
|
||||
/output/
|
||||
/src/examples/build/
|
||||
# ifctester docs output
|
||||
/src/ifctester/test/build/
|
||||
|
||||
# output directories
|
||||
/cmake/out/
|
||||
@@ -16,7 +12,6 @@
|
||||
/src/ifcmax/out/
|
||||
/src/ifcwrap/out/
|
||||
/src/qtviewer/out/
|
||||
/src/ifctester/webapp/public/pyodide/
|
||||
|
||||
/win/BuildDepsCache*.txt
|
||||
|
||||
@@ -24,14 +19,12 @@
|
||||
__pycache__
|
||||
*.py.bak
|
||||
venv
|
||||
uv.lock
|
||||
|
||||
# Visual Studio Code files
|
||||
.vscode
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
.vs
|
||||
/*.code-workspace
|
||||
|
||||
# PyCharm files
|
||||
.idea
|
||||
@@ -87,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/
|
||||
@@ -126,11 +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
|
||||
CLAUDE.local.md
|
||||
*.py.tmp*
|
||||
*.json.tmp*
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil
|
||||
|
||||
For more information, see:
|
||||
|
||||
* [IfcOpenShell Website](https://ifcopenshell.org)
|
||||
* [IfcOpenShell Website](http://ifcopenshell.org)
|
||||
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
|
||||
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
|
||||
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
|
||||
@@ -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}")
|
||||
|
||||
+14
-28
@@ -27,14 +27,13 @@ endif()
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# The VERSION file in the repository root is the single source of truth for the
|
||||
# release version. Read it unconditionally so a plain source build reports the
|
||||
# real version through buildinfo.cpp instead of the stale hardcoded 0.8.0
|
||||
# fallback (see #8164). VERSION_OVERRIDE still controls the branch name used
|
||||
# when ADD_COMMIT_SHA embeds a commit sha.
|
||||
file(READ "../VERSION" "RELEASE_VERSION_")
|
||||
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
|
||||
message(STATUS "Detected version '${RELEASE_VERSION}'")
|
||||
if(VERSION_OVERRIDE)
|
||||
file(READ "../VERSION" "RELEASE_VERSION_")
|
||||
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
|
||||
message(STATUS "Detected version '${RELEASE_VERSION}'")
|
||||
else()
|
||||
set(RELEASE_VERSION "0.8.0")
|
||||
endif()
|
||||
|
||||
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
|
||||
|
||||
@@ -259,14 +258,10 @@ if(WITH_ROCKSDB)
|
||||
set(ROCKSDB_LIBRARIES "IFCOPENSHELL_RocksDB")
|
||||
target_compile_definitions(IFCOPENSHELL_RocksDB INTERFACE IFOPSH_WITH_ROCKSDB)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB)
|
||||
# Shared binaries for `rocksdb` only support limited API (only `c.h`), but we use `db.h` API.
|
||||
# So rocksdb supported only as a static library.
|
||||
# See https://github.com/facebook/rocksdb/issues/981.
|
||||
if(TARGET RocksDB::rocksdb)
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb)
|
||||
elseif(TARGET RocksDB::rocksdb-shared)
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb-shared)
|
||||
else()
|
||||
message(FATAL_ERROR "RocksDB found but neither RocksDB::rocksdb nor RocksDB::rocksdb-shared target exists")
|
||||
endif()
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb)
|
||||
|
||||
if(WITH_ZSTD)
|
||||
# @todo do we actually need the zstd include dir or rather just pass
|
||||
@@ -314,12 +309,8 @@ if(WASM_BUILD)
|
||||
else()
|
||||
# @todo review this, shouldn't this be all possible header-only now?
|
||||
# ... or rewritten using C++17 features?
|
||||
# Boost.System has been header-only since 1.69 and its compiled stub library
|
||||
# was dropped in newer Boost, so requesting it as a component makes
|
||||
# find_package fail on Boost 1.70 and up (for example Boost 1.90). It is
|
||||
# still pulled in transitively by thread / iostreams where needed, so do not
|
||||
# request it explicitly.
|
||||
set(BOOST_COMPONENTS
|
||||
system
|
||||
program_options
|
||||
regex
|
||||
thread
|
||||
@@ -563,8 +554,8 @@ if(COMPILE_SCHEMA)
|
||||
# Bootstrap the parser
|
||||
message(STATUS "Compiling schema, this will take a while...")
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py
|
||||
WORKING_DIRECTORY ../src/ifcopenshell-python/ifcopenshell/express
|
||||
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py express.bnf
|
||||
WORKING_DIRECTORY ../src/ifcexpressparser
|
||||
OUTPUT_FILE express_parser.py
|
||||
RESULT_VARIABLE SUCCESS
|
||||
)
|
||||
@@ -575,7 +566,7 @@ if(COMPILE_SCHEMA)
|
||||
|
||||
# Generate code
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} ../ifcopenshell-python/ifcopenshell/express/express_parser.py ../../${COMPILE_SCHEMA}
|
||||
COMMAND ${PYTHON_EXECUTABLE} ../ifcexpressparser/express_parser.py ../../${COMPILE_SCHEMA}
|
||||
WORKING_DIRECTORY ../src/ifcparse
|
||||
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME
|
||||
)
|
||||
@@ -665,11 +656,6 @@ if(ADD_COMMIT_SHA)
|
||||
endif()
|
||||
endif(ADD_COMMIT_SHA)
|
||||
|
||||
# Always expose the release version (from the VERSION file) to buildinfo.cpp so
|
||||
# that a build without commit-sha info reports the correct version instead of a
|
||||
# stale hardcoded fallback. See #8164.
|
||||
target_compile_definitions(IfcParse PRIVATE IFCOPENSHELL_VERSION_STRING=${RELEASE_VERSION})
|
||||
|
||||
if(MSVC)
|
||||
# @todo still needs to be understood better, but the cgal and cgal-simple kernel cause multiply defined boost lambda placeholders _1 ... _3
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
|
||||
|
||||
@@ -88,15 +88,7 @@ if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
|
||||
mark_as_advanced(HDF5_DIR)
|
||||
if(HDF5_DIR)
|
||||
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
|
||||
if(TARGET hdf5_cpp-static)
|
||||
set(HDF5_LIBRARIES hdf5_cpp-static)
|
||||
elseif(TARGET hdf5_cpp-shared)
|
||||
set(HDF5_LIBRARIES hdf5_cpp-shared)
|
||||
elseif(TARGET hdf5::hdf5_cpp-shared)
|
||||
set(HDF5_LIBRARIES hdf5::hdf5_cpp-shared)
|
||||
else()
|
||||
find_package(HDF5 REQUIRED COMPONENTS CXX)
|
||||
endif()
|
||||
set(HDF5_LIBRARIES hdf5_cpp-static)
|
||||
else()
|
||||
# If it failed, still try to find as a module.
|
||||
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
################################################################################
|
||||
# #
|
||||
# This file is part of IfcOpenShell. #
|
||||
# #
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the Lesser GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3.0 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# IfcOpenShell 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 #
|
||||
# Lesser GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the Lesser GNU General Public License #
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
################################################################################
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/utilities.cmake" OPTIONAL)
|
||||
|
||||
set(_IfcOpenShell_find_args)
|
||||
if(IfcOpenShell_FIND_VERSION)
|
||||
list(APPEND _IfcOpenShell_find_args "${IfcOpenShell_FIND_VERSION}")
|
||||
if(IfcOpenShell_FIND_VERSION_EXACT)
|
||||
list(APPEND _IfcOpenShell_find_args EXACT)
|
||||
endif()
|
||||
endif()
|
||||
list(APPEND _IfcOpenShell_find_args CONFIG QUIET)
|
||||
if(IfcOpenShell_FIND_COMPONENTS)
|
||||
list(APPEND _IfcOpenShell_find_args COMPONENTS ${IfcOpenShell_FIND_COMPONENTS})
|
||||
endif()
|
||||
|
||||
set(_IfcOpenShell_saved_module_path "${CMAKE_MODULE_PATH}")
|
||||
list(REMOVE_ITEM CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}")
|
||||
find_package(IfcOpenShell ${_IfcOpenShell_find_args})
|
||||
set(CMAKE_MODULE_PATH "${_IfcOpenShell_saved_module_path}")
|
||||
|
||||
if(NOT IfcOpenShell_FOUND)
|
||||
set(_IfcOpenShell_error "Could not find an IfcOpenShell CMake config package. Set IfcOpenShell_DIR or CMAKE_PREFIX_PATH.")
|
||||
if(IfcOpenShell_FIND_REQUIRED)
|
||||
message(FATAL_ERROR "${_IfcOpenShell_error}")
|
||||
elseif(NOT IfcOpenShell_FIND_QUIETLY)
|
||||
message(STATUS "${_IfcOpenShell_error}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(_IfcOpenShell_required_targets IfcOpenShell::IfcParse IfcOpenShell::IfcGeom)
|
||||
set(_IfcOpenShell_missing_targets "")
|
||||
foreach(_IfcOpenShell_target IN LISTS _IfcOpenShell_required_targets)
|
||||
if(NOT TARGET ${_IfcOpenShell_target})
|
||||
list(APPEND _IfcOpenShell_missing_targets ${_IfcOpenShell_target})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(_IfcOpenShell_missing_targets)
|
||||
set(IfcOpenShell_FOUND FALSE)
|
||||
string(REPLACE ";" ", " _IfcOpenShell_missing_targets_text "${_IfcOpenShell_missing_targets}")
|
||||
set(_IfcOpenShell_error "IfcOpenShell config was found, but required targets are missing: ${_IfcOpenShell_missing_targets_text}.")
|
||||
if(IfcOpenShell_FIND_REQUIRED)
|
||||
message(FATAL_ERROR "${_IfcOpenShell_error}")
|
||||
elseif(NOT IfcOpenShell_FIND_QUIETLY)
|
||||
message(STATUS "${_IfcOpenShell_error}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED IFCOPENSHELL_WITH_OPENCASCADE)
|
||||
set(IFCOPENSHELL_WITH_OPENCASCADE OFF)
|
||||
if(TARGET IfcOpenShell::geometry_kernel_opencascade)
|
||||
set(IFCOPENSHELL_WITH_OPENCASCADE ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED IFCOPENSHELL_WITH_CGAL)
|
||||
set(IFCOPENSHELL_WITH_CGAL OFF)
|
||||
if(TARGET IfcOpenShell::IFCOPENSHELL_CGAL)
|
||||
set(IFCOPENSHELL_WITH_CGAL ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED IFCOPENSHELL_IFCXML)
|
||||
set(IFCOPENSHELL_IFCXML OFF)
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED IFCOPENSHELL_WITH_ROCKSDB)
|
||||
set(IFCOPENSHELL_WITH_ROCKSDB OFF)
|
||||
endif()
|
||||
|
||||
set(IFCOPENSHELL_LIBRARIES IfcOpenShell::IfcParse)
|
||||
foreach(_IfcOpenShell_target IN ITEMS IfcOpenShell::geometry_serializer IfcOpenShell::Serializers)
|
||||
if(TARGET ${_IfcOpenShell_target})
|
||||
list(APPEND IFCOPENSHELL_LIBRARIES ${_IfcOpenShell_target})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
set(IFCOPENSHELL_KERNEL_LIBRARIES "")
|
||||
foreach(_IfcOpenShell_target IN ITEMS
|
||||
IfcOpenShell::geometry_kernel_opencascade
|
||||
IfcOpenShell::geometry_kernel_cgal
|
||||
IfcOpenShell::geometry_kernel_cgal_simple
|
||||
)
|
||||
if(TARGET ${_IfcOpenShell_target})
|
||||
list(APPEND IFCOPENSHELL_KERNEL_LIBRARIES ${_IfcOpenShell_target})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
set(IFCOPENSHELL_GEOMETRY_LIBRARIES IfcOpenShell::IfcGeom ${IFCOPENSHELL_KERNEL_LIBRARIES})
|
||||
|
||||
if(TARGET IfcOpenShell::OpenCASCADE_INTERFACE)
|
||||
set(OpenCASCADE_LIBRARIES IfcOpenShell::OpenCASCADE_INTERFACE)
|
||||
endif()
|
||||
|
||||
if(TARGET IfcOpenShell::IFCOPENSHELL_CGAL)
|
||||
set(CGAL_LIBRARIES IfcOpenShell::IFCOPENSHELL_CGAL)
|
||||
endif()
|
||||
|
||||
if(TARGET IfcOpenShell::svgfill)
|
||||
set(IFCOPENSHELL_SVGFILL_LIBRARY IfcOpenShell::svgfill)
|
||||
endif()
|
||||
|
||||
mark_as_advanced(IfcOpenShell_DIR)
|
||||
|
||||
unset(_IfcOpenShell_error)
|
||||
unset(_IfcOpenShell_find_args)
|
||||
unset(_IfcOpenShell_missing_targets)
|
||||
unset(_IfcOpenShell_missing_targets_text)
|
||||
unset(_IfcOpenShell_required_targets)
|
||||
unset(_IfcOpenShell_target)
|
||||
@@ -7,26 +7,12 @@ set(IFCOPENSHELL_WITH_OPENCASCADE @WITH_OPENCASCADE@)
|
||||
set(IFCOPENSHELL_WITH_CGAL @WITH_CGAL@)
|
||||
set(IFCOPENSHELL_IFCXML @IFCXML_SUPPORT@)
|
||||
set(IFCOPENSHELL_WITH_ROCKSDB @WITH_ROCKSDB@)
|
||||
set(IFCOPENSHELL_COLLADA_SUPPORT @COLLADA_SUPPORT@)
|
||||
set(IFCOPENSHELL_GLTF_SUPPORT @GLTF_SUPPORT@)
|
||||
set(IFCOPENSHELL_HDF5_SUPPORT @HDF5_SUPPORT@)
|
||||
set(IFCOPENSHELL_WITH_PROJ @WITH_PROJ@)
|
||||
set(IFCOPENSHELL_USD_SUPPORT @USD_SUPPORT@)
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
|
||||
set(IFCOPENSHELL_BOOST_USE_STATIC_LIBS "@Boost_USE_STATIC_LIBS@")
|
||||
set(IFCOPENSHELL_BOOST_USE_STATIC_RUNTIME "@Boost_USE_STATIC_RUNTIME@")
|
||||
set(IFCOPENSHELL_BOOST_USE_MULTITHREADED "@Boost_USE_MULTITHREADED@")
|
||||
if(NOT "${IFCOPENSHELL_BOOST_USE_STATIC_LIBS}" STREQUAL "")
|
||||
set(Boost_USE_STATIC_LIBS ${IFCOPENSHELL_BOOST_USE_STATIC_LIBS})
|
||||
endif()
|
||||
if(NOT "${IFCOPENSHELL_BOOST_USE_STATIC_RUNTIME}" STREQUAL "")
|
||||
set(Boost_USE_STATIC_RUNTIME ${IFCOPENSHELL_BOOST_USE_STATIC_RUNTIME})
|
||||
endif()
|
||||
if(NOT "${IFCOPENSHELL_BOOST_USE_MULTITHREADED}" STREQUAL "")
|
||||
set(Boost_USE_MULTITHREADED ${IFCOPENSHELL_BOOST_USE_MULTITHREADED})
|
||||
endif()
|
||||
set(Boost_USE_STATIC_LIBS ON)
|
||||
set(Boost_USE_STATIC_RUNTIME OFF)
|
||||
set(Boost_USE_MULTITHREADED ON)
|
||||
set(Boost_COMPONENTS
|
||||
system
|
||||
program_options
|
||||
@@ -57,33 +43,13 @@ if(IFCOPENSHELL_WITH_ROCKSDB)
|
||||
endif()
|
||||
|
||||
if(IFCOPENSHELL_IFCXML)
|
||||
find_dependency(LibXml2)
|
||||
find_dependency(LibXml2 CONFIG)
|
||||
endif()
|
||||
|
||||
if(IFCOPENSHELL_WITH_CGAL)
|
||||
find_dependency(CGAL CONFIG)
|
||||
endif()
|
||||
|
||||
if(IFCOPENSHELL_COLLADA_SUPPORT)
|
||||
find_dependency(OpenCOLLADA)
|
||||
endif()
|
||||
|
||||
if(IFCOPENSHELL_GLTF_SUPPORT)
|
||||
find_dependency(nlohmann_json CONFIG)
|
||||
endif()
|
||||
|
||||
if(IFCOPENSHELL_HDF5_SUPPORT)
|
||||
find_dependency(HDF5 COMPONENTS C CXX)
|
||||
endif()
|
||||
|
||||
if(IFCOPENSHELL_WITH_PROJ)
|
||||
find_dependency(PROJ)
|
||||
endif()
|
||||
|
||||
if(IFCOPENSHELL_USD_SUPPORT)
|
||||
find_dependency(USD)
|
||||
endif()
|
||||
|
||||
if(IFCOPENSHELL_WITH_OPENCASCADE)
|
||||
find_dependency(OpenCASCADE CONFIG)
|
||||
if(OpenCASCADE_VERSION VERSION_LESS "7.7.0")
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.env
|
||||
*.pyc
|
||||
__pycache__
|
||||
@@ -1,3 +0,0 @@
|
||||
.env
|
||||
*.pyc
|
||||
__pycache__
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# .ifcos_env
|
||||
# register autocompletes. just source the file in your shell, i.e.
|
||||
# source .ifcos_env
|
||||
|
||||
.ifcos_env() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
opts="create update up down restart build attach logs ps config remove help"
|
||||
|
||||
# Basic static completion
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Register the completion for the command "ifcos_env"
|
||||
complete -F .ifcos_env ./ifcos_env
|
||||
@@ -1,67 +0,0 @@
|
||||
FROM rockylinux:9
|
||||
|
||||
# Update system, enable CRB (needed by some EPEL packages) and install EPEL,
|
||||
# then install required packages + some common tools for a bit of command
|
||||
# line comfort. Combined into one layer so a later `create` always installs
|
||||
# against packages from the same dnf update, rather than layering fresh
|
||||
# installs on top of a stale cached "update" layer.
|
||||
RUN dnf update -y && \
|
||||
dnf install -y epel-release && \
|
||||
dnf config-manager --set-enabled crb && \
|
||||
dnf install -y --allowerasing --setopt=install_weak_deps=False --setopt=tsflags=nodocs \
|
||||
bash-completion vim git curl wget which tree htop sudo \
|
||||
gcc gcc-c++ autoconf automake bison make zip cmake \
|
||||
python3 python3-pip \
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libuuid-devel git-lfs \
|
||||
findutils xz byacc ccache && \
|
||||
git lfs install --system && \
|
||||
dnf clean all && \
|
||||
rm -rf /var/cache/dnf
|
||||
|
||||
# Trust bind-mounted repos regardless of which user (root or builder) or host
|
||||
# UID owns them, rather than a per-user config that only one of them sees.
|
||||
RUN git config --system --add safe.directory '*'
|
||||
|
||||
# Configure ccache. CCACHE_MAXSIZE (not `ccache -M`) because /ccache is a
|
||||
# volume mount point at runtime - anything `ccache -M` writes to a config
|
||||
# file under it during this build gets shadowed once the real volume is
|
||||
# mounted, so the size cap only actually takes effect via the env var.
|
||||
# 2G is generous: a full build (IfcParse+IfcGeom+IfcConvert+wrapper, one
|
||||
# Python version) measures ~300MB, and the volume is now shared across all
|
||||
# checkouts (see compose.yaml), so this covers several diverging branches.
|
||||
ENV CCACHE_DIR=/ccache
|
||||
ENV CCACHE_MAXSIZE=2G
|
||||
ENV PATH="/usr/lib/ccache:$PATH"
|
||||
|
||||
# Non-root user matching the host UID/GID that bind-mounts the repo (default
|
||||
# 1000:1000, the common single-user-Linux-box case), so files the build
|
||||
# creates under the mount keep sane, non-root ownership on the host side.
|
||||
# Override with --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g)
|
||||
# if your host user has a different UID/GID.
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=1000
|
||||
# groupadd fails outright if USER_GID is already taken by an existing
|
||||
# system group - which happens whenever a host's primary GID collides with
|
||||
# one baked into the rockylinux9 base image. The main real-world case is
|
||||
# macOS, where the default user's primary group is "staff" at GID 20, and
|
||||
# GID 20 is "games" on RHEL-family images. Only create the "builder" group
|
||||
# when that GID is actually free; otherwise useradd just attaches to
|
||||
# whichever group already owns it. Either way the builder user ends up
|
||||
# with the right GID for bind-mount ownership, which is all that matters.
|
||||
RUN (getent group "${USER_GID}" >/dev/null || groupadd -g "${USER_GID}" builder) \
|
||||
&& useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash builder \
|
||||
&& echo "builder ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/builder
|
||||
|
||||
# Copied while still root: /bin is not writable by the builder user.
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.11.27 /uv /uvx /bin/
|
||||
|
||||
USER builder
|
||||
WORKDIR /__w/IfcOpenShell/IfcOpenShell
|
||||
|
||||
# Installed as builder so managed Python interpreters land under builder's
|
||||
# $HOME, matching the user that actually runs the build.
|
||||
RUN uv python install
|
||||
|
||||
CMD ["sleep", "infinity"]
|
||||
@@ -1,78 +0,0 @@
|
||||
Docker build environment
|
||||
========================
|
||||
|
||||
This is a small utility to make it easy to compile a perfect `_ifcopenshell_wrapper.cpython-*-x86_64-linux-gnu.so`
|
||||
files.
|
||||
|
||||
The reason for this tool is that I was trying to follow the web page directions, and my build was behaving differently
|
||||
to the release builds. Eventually I concluded that the differences between toolchains on the RHEL based rocky9 image
|
||||
and Ubuntu were just too great. Getting the build setup was already a lot of trial and error, so I thought I'd spend
|
||||
more time trying to reuse the github actions that perform the build, using a utility called `act`. I learnt a lot, in
|
||||
particular how much time, energy, and bandwidth Github waste. I also realised I was most of the way to a regular docker
|
||||
setup anyway, so I might as well just do that. So I've deconstructed all the github action steps, and turned it into
|
||||
a local docker build environment that uses the exact same base, tools, libraries, and build command/flags etc.
|
||||
|
||||
Right now a Github action will:
|
||||
- launch the rocky9 base
|
||||
- upgrade all the packages
|
||||
- install a bunch of extra tools
|
||||
- do a recursive checkout of your repo
|
||||
- checkout the build repository
|
||||
- unpack dependencies
|
||||
- run the build script, making all python versions (5? right now I think)
|
||||
- create the .zip release files
|
||||
|
||||
And it does _all_ of that _every_ time. This is not a fault of the action writers - it's just how Github seems to work.
|
||||
|
||||
These dockers tools do the following differently, and it's actually a bit more powerful too:
|
||||
- build the base image once.
|
||||
- update the packages once.
|
||||
- install the extra tools once.
|
||||
- the repository is the one on your host, that gets bind mounted in the container as the working directory.
|
||||
- by adding an environment variable to .env, restricts to compiling for just a single python version.
|
||||
- when the build is finished the created files are right there under your local repositry (but not added to git) for
|
||||
ease of access
|
||||
- each repository can have it's own build environment container.
|
||||
- the image is shared between those environments.
|
||||
- the containers share the ccache, so additional envs should get a helping hand.
|
||||
- it has a simple set of user friendly commands to drive it all.
|
||||
|
||||
For example:
|
||||
``` bash
|
||||
# To see the commands (a superset of docker compose commands)
|
||||
./ifcos_env
|
||||
|
||||
# Enable autocomplete of commands
|
||||
source .ifcos_env
|
||||
|
||||
# First time commands
|
||||
./ifcos_env create
|
||||
./ifcos_env up
|
||||
./ifcos_env build
|
||||
|
||||
# install and test library
|
||||
# find an issue
|
||||
# edit code
|
||||
./ifcos_env build
|
||||
|
||||
# and so on. When done stop and optionally delete the container
|
||||
./ifcos_env stop
|
||||
./ifcos_env remove
|
||||
```
|
||||
|
||||
To limit the build to one python version just add
|
||||
``` bash
|
||||
PY_TGT=py-311
|
||||
```
|
||||
or whichever version your Blender requires.
|
||||
|
||||
You might see UNIQUE_ID in the .env file too. This keeps containers for separate folders, separate.
|
||||
|
||||
System requirements
|
||||
1. Linux-x64 only at this time.
|
||||
2. Docker and docker-compose need to be installed.
|
||||
3. Have a good amount of disk space. (image is in /var (typically the root partition) and will be about 1.7 GB)
|
||||
4. The build action will create about 10GB in your repository folder. Make sure this partition is spacious
|
||||
particularly if you intent on having multiple clones building.
|
||||
5. ... I think that covers most of it.
|
||||
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
---
|
||||
name: ifcopenshell-docker-build
|
||||
description: >-
|
||||
Build a real ifcopenshell_wrapper (.so + .py) and IfcConvert locally via
|
||||
the docker/ifcos_env toolchain, then wire them into a checkout for
|
||||
running C++-dependent parts of the test suite (geometry, the SWIG
|
||||
wrapper stub, the C++ parser). Use whenever a task needs to compile
|
||||
IfcOpenShell's C++ core rather than just read/patch source - e.g.
|
||||
reproducing or fixing a bug in src/ifcgeom, src/ifcparse, src/ifcwrap,
|
||||
or validating util/scripts/validate_stub.py against the actual
|
||||
generated wrapper.
|
||||
---
|
||||
|
||||
# Building IfcOpenShell locally with docker/ifcos_env
|
||||
|
||||
`docker/` mirrors the project's GitHub Actions build environment locally,
|
||||
in a persistent, non-root container with ccache so repeat builds are fast.
|
||||
See `docker/README.md` for the design rationale. Pure-Python changes don't
|
||||
need any of this - only reach for it when you need a real compiled
|
||||
`_ifcopenshell_wrapper*.so` or `IfcConvert` binary.
|
||||
|
||||
## Placement
|
||||
|
||||
This `docker/` folder must live as a direct child of the repo root you want
|
||||
to build (sibling of `src/`, `cmake/`, etc.) - `compose.yaml` and
|
||||
`ifcos_env` resolve the repo via `../` relative to wherever `docker/`
|
||||
itself sits, and bind-mount it into the container. If you're setting this
|
||||
up in a fresh clone, copy the whole `docker/` directory there first.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
./ifcos_env create # build the image (shared by name across all your clones/checkouts, so usually instant after the first time anywhere)
|
||||
./ifcos_env up # create + start the container, clone/unpack the third-party dependency cache (~10GB, one-time per container)
|
||||
./ifcos_env build # full build: all deps + IfcParse + IfcGeom + IfcConvert + the Python wrapper, for one Python version
|
||||
```
|
||||
|
||||
`PY_TGT` and `UNIQUE_ID` live in `docker/.env` - `PY_TGT` (e.g. `py-311`)
|
||||
restricts the build to one Python version instead of building five;
|
||||
`UNIQUE_ID` is a hash of the folder path, recalculated on every `up`, so
|
||||
each checkout gets its own container/volumes automatically.
|
||||
|
||||
A full first build takes ~1.5 hours (mostly compiling IfcOpenShell's own
|
||||
C++, not the cached third-party deps). After that, ccache makes incremental
|
||||
rebuilds of a couple of touched `.cpp` files **under a minute**.
|
||||
|
||||
## Container lifecycle
|
||||
|
||||
The container is long-lived (`sleep infinity`) so exec'd commands and
|
||||
ccache state persist between builds. Commands map directly onto Docker
|
||||
Compose's own container-vs-image distinction:
|
||||
|
||||
```bash
|
||||
./ifcos_env up # create the container if it doesn't exist, then start it (runs ready_repo too)
|
||||
./ifcos_env stop # stop the container, keep it around
|
||||
./ifcos_env start # start it back up (same container, same filesystem layer)
|
||||
./ifcos_env restart # stop, then start
|
||||
./ifcos_env down # remove the container (and its network) entirely
|
||||
./ifcos_env recreate # down, then up - a fresh container
|
||||
```
|
||||
|
||||
Named volumes (`ccache`) and the bind-mounted repo/`build/` are unaffected
|
||||
by `down`/`recreate` - only the container itself goes away, and `up`
|
||||
recreates it from the image.
|
||||
|
||||
## Fast iteration
|
||||
|
||||
Pass a target to `build` to skip the parts you don't need:
|
||||
|
||||
```bash
|
||||
./ifcos_env build IfcConvert # only the executables (IfcConvert, IfcGeomServer) - skips the Python wrapper entirely
|
||||
./ifcos_env build IfcOpenShell-Python # only the SWIG Python wrapper - skips executables entirely
|
||||
./ifcos_env build # no target = everything (needed the first time, or after touching shared headers)
|
||||
```
|
||||
|
||||
Use this to keep the edit -> rebuild -> test loop fast when debugging: if
|
||||
you're only touching `src/ifcgeom/`, build `IfcConvert`; if you're only
|
||||
exercising the Python API, build `IfcOpenShell-Python`.
|
||||
|
||||
## Where the artifacts land
|
||||
|
||||
Build output goes to `<repo_root>/build/Linux/x86_64/install/` on the host
|
||||
(bind-mounted, not just inside the container), owned by you (see
|
||||
"Container user" below):
|
||||
|
||||
- `ifcopenshell/bin/IfcConvert` - the CLI binary
|
||||
- `python-<version>/lib/python<X.Y>/site-packages/ifcopenshell/_ifcopenshell_wrapper*.so`
|
||||
and `ifcopenshell_wrapper.py` - the compiled wrapper + its generated
|
||||
Python glue
|
||||
|
||||
## Testing against a checkout (automated / AI-driven)
|
||||
|
||||
`_ifcopenshell_wrapper*.so` and `ifcopenshell_wrapper.py` are already
|
||||
gitignored under `src/ifcopenshell-python/ifcopenshell/`, which is exactly
|
||||
where a normal in-tree build would put them - copy the two files there:
|
||||
|
||||
```bash
|
||||
SRC=build/Linux/x86_64/install/python-3.11.8/lib/python3.11/site-packages/ifcopenshell
|
||||
cp "$SRC/_ifcopenshell_wrapper.cpython-311-x86_64-linux-gnu.so" src/ifcopenshell-python/ifcopenshell/
|
||||
cp "$SRC/ifcopenshell_wrapper.py" src/ifcopenshell-python/ifcopenshell/
|
||||
```
|
||||
|
||||
Then, to run the test suite against it:
|
||||
|
||||
```bash
|
||||
export PATH="$PWD/build/Linux/x86_64/install/ifcopenshell/bin:$PATH" # for IfcConvert-dependent tests
|
||||
cd src/ifcopenshell-python/test
|
||||
PYTHONPATH="$PWD/.." python3.11 -m pytest -p no:pytest-blender .
|
||||
```
|
||||
|
||||
(`-p no:pytest-blender` avoids the pytest-blender plugin trying to find a
|
||||
`blender` executable and failing collection entirely, even for non-Blender
|
||||
tests.) You'll need the matching Python version's `pip install`s too
|
||||
(numpy, shapely, isodate, lark, tabulate, pytest, ... - whatever the
|
||||
modules under test import) since this is a bare interpreter, not the
|
||||
project's pixi env.
|
||||
|
||||
**This is the pattern to use for automated or AI-driven verification.**
|
||||
Don't use `try` (below) for that - it overwrites files in a real, live
|
||||
Blender installation, which isn't something an automated/AI workflow
|
||||
should ever do without the human explicitly asking for it in the moment.
|
||||
|
||||
## Testing in Blender itself (human only)
|
||||
|
||||
`try` copies the built wrapper straight into your actual Blender/Bonsai
|
||||
extension install, for manual in-Blender testing:
|
||||
|
||||
```bash
|
||||
./ifcos_env try
|
||||
```
|
||||
|
||||
It reads `BLENDER_USER_RESOURCE` from `.env` - set this to wherever
|
||||
Blender's user resource folder for the Bonsai extension actually lives on
|
||||
your system, which depends on your own Blender setup:
|
||||
|
||||
```bash
|
||||
# in docker/.env
|
||||
BLENDER_USER_RESOURCE=~/.config/blender/bonsai/
|
||||
```
|
||||
|
||||
`try` figures out the built Python version from `build/.../install/`
|
||||
(disambiguating with `PY_TGT` if more than one version was built) and
|
||||
copies the wrapper to
|
||||
`$BLENDER_USER_RESOURCE/extensions/.local/lib/python<X.Y>/site-packages/ifcopenshell/`.
|
||||
|
||||
## Container user
|
||||
|
||||
The image runs as a non-root `builder` user, UID/GID matching your host
|
||||
account (passed as `--build-arg` by `create` from `id -u`/`id -g`, so it
|
||||
adjusts automatically - no manual flag needed even if you're not 1000:1000).
|
||||
Files the build creates under the bind mount come out owned by you, not
|
||||
root. Passwordless `sudo` is available inside the container (e.g. via
|
||||
`attach`) for the rare case you need root for something ad hoc.
|
||||
|
||||
If you're picking up an existing checkout that was previously built with
|
||||
an older, root-based image, you may hit `Permission denied` the first time
|
||||
you run `up`/`build` under the new image - `build/`, `.git/modules/`, the
|
||||
`ccache` volume, `output/`, and `build.log` can all be left root-owned from
|
||||
before. Fix it once via the container's own root (no host `sudo` needed):
|
||||
|
||||
```bash
|
||||
docker exec -u root -w /__w/IfcOpenShell/IfcOpenShell <container-name> \
|
||||
chown -R "$(id -u)":"$(id -g)" .git/modules build output build.log /ccache
|
||||
```
|
||||
|
||||
(`<container-name>` is `ifcopenshell-<UNIQUE_ID>` - see `docker ps -a`.)
|
||||
|
||||
## Other things worth knowing
|
||||
|
||||
- **Linux x64 only.** `compose.yaml` pins `platform: linux/amd64`; on an
|
||||
ARM host (e.g. Apple Silicon) this build isn't available.
|
||||
- **The final "Package .zip archives" step of `build()` has a pre-existing
|
||||
bash syntax error**, unrelated to compilation - the actual build already
|
||||
succeeded by that point (look for `Built IfcOpenShell...` in the output),
|
||||
so this is safe to ignore if you only need the raw artifacts under
|
||||
`build/.../install/`, not packaged release zips.
|
||||
- **`test_mmaped_stream` and similar `USE_MMAP`-dependent tests will fail**
|
||||
against this build - `nix/build-all.py` is invoked with `USE_MMAP=OFF`
|
||||
here. Not a bug in your code if you see it fail.
|
||||
- Only the bind-mounted `<repo>/build` lives on the host filesystem your
|
||||
repo is checked out on. Anything the container writes *outside* that
|
||||
mount lives in the container's own writable layer under Docker's data
|
||||
root (commonly `/var/lib/docker`, i.e. usually your root partition) -
|
||||
keep an eye on `df -h /` if you're running several of these containers
|
||||
at once.
|
||||
@@ -1,15 +0,0 @@
|
||||
name: ifcopenshell-${UNIQUE_ID}
|
||||
services:
|
||||
ifcopenshell:
|
||||
container_name: ifcopenshell-${UNIQUE_ID}
|
||||
image: ifcopenshell-build-env:updated
|
||||
platform: linux/amd64
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ../
|
||||
target: /__w/IfcOpenShell/IfcOpenShell
|
||||
- ccache:/ccache
|
||||
|
||||
volumes:
|
||||
ccache:
|
||||
name: ifcopenshell-ccache-shared
|
||||
@@ -1,339 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ================== CONFIG ==================
|
||||
SCRIPT_NAME=$(basename "$0")
|
||||
ENV_FILE=".env"
|
||||
WORKDIR="/__w/IfcOpenShell/IfcOpenShell"
|
||||
NAMEPREFIX=ifcopenshell
|
||||
|
||||
function set_env() {
|
||||
# Load .env file if it exists
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
echo "✅ Loaded environment variables from $ENV_FILE"
|
||||
else
|
||||
echo "⚠️ No $ENV_FILE found, proceeding without it."
|
||||
fi
|
||||
}
|
||||
|
||||
set_env
|
||||
|
||||
# ================ FUNCTIONS =================
|
||||
|
||||
function create() {
|
||||
echo "⭐ Creating image: ifcopenshell-build-env"
|
||||
docker build -f Dockerfile \
|
||||
--build-arg USER_UID="$(id -u)" --build-arg USER_GID="$(id -g)" \
|
||||
-t ifcopenshell-build-env:updated .
|
||||
}
|
||||
|
||||
function update() {
|
||||
# The Dockerfile always builds FROM a clean rockylinux:9 and does
|
||||
# `dnf update -y` as its first step, so re-running create() is enough
|
||||
# to get fresh packages.
|
||||
echo "⚡ Updating image: ifcopenshell-build-env"
|
||||
create
|
||||
}
|
||||
|
||||
function up() {
|
||||
# Creates the container if it doesn't exist yet (and starts it either
|
||||
# way) - this is the one that needs ready_repo, since a freshly created
|
||||
# container has no submodules/dependency cache in place yet.
|
||||
echo "🚀 Creating/starting stack: ifcopenshell-${UNIQUE_ID}"
|
||||
unique # Update UNIQUE_ID first
|
||||
docker compose up -d "$@" # Container must exist before ready_repo can exec into it.
|
||||
ready_repo # Ensure repo is recursive, and the build repo is in place.
|
||||
}
|
||||
|
||||
function down() {
|
||||
# Removes the container (and its network) entirely. Named volumes
|
||||
# (ccache) and the bind-mounted repo/build/ survive; up() will recreate
|
||||
# the container from scratch next time.
|
||||
echo "🔥 Removing stack: ifcopenshell-${UNIQUE_ID}"
|
||||
docker compose down "$@"
|
||||
}
|
||||
|
||||
function stop() {
|
||||
# Stops the existing container without removing it - the container,
|
||||
# its filesystem layer, and its exec history all remain intact.
|
||||
echo "🛑 Stopping stack: ifcopenshell-${UNIQUE_ID}"
|
||||
docker compose stop "$@"
|
||||
}
|
||||
|
||||
function start() {
|
||||
# Starts a previously-stopped container back up. Does nothing (and
|
||||
# won't create anything) if the container doesn't exist - use up() for
|
||||
# that.
|
||||
echo "▶️ Starting stack: ifcopenshell-${UNIQUE_ID}"
|
||||
docker compose start "$@"
|
||||
}
|
||||
|
||||
function restart() {
|
||||
echo "🔄 Restarting stack (stop, then start)..."
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
function recreate() {
|
||||
echo "♻️ Recreating stack (down, then up)..."
|
||||
down
|
||||
up
|
||||
}
|
||||
|
||||
function logs() {
|
||||
echo "📜 Showing logs..."
|
||||
docker compose logs -f "$@"
|
||||
}
|
||||
|
||||
function ps() {
|
||||
docker compose ps
|
||||
}
|
||||
|
||||
function config() {
|
||||
echo "🔍 Validated compose configuration:"
|
||||
docker compose config
|
||||
}
|
||||
|
||||
function remove() {
|
||||
# Lower-level than down(): removes already-stopped containers without
|
||||
# touching the compose network. Mostly useful after a plain stop().
|
||||
echo "🗑️ Removing stopped containers: ifcopenshell-${UNIQUE_ID}"
|
||||
docker compose rm "$@"
|
||||
}
|
||||
|
||||
function unique() {
|
||||
echo "🔧 Making stack name folder specific..."
|
||||
|
||||
REGEX="^UNIQUE_ID="
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]] || ! grep -qE "$REGEX" "$ENV_FILE"; then
|
||||
echo -e "\nUNIQUE_ID=dummy\n" >> "$ENV_FILE"
|
||||
fi
|
||||
|
||||
export UNIQUE_ID="$(pwd | sha256sum | cut -c -8)"
|
||||
|
||||
# `sed -i` takes incompatible syntax between GNU sed (Linux) and BSD sed
|
||||
# (macOS) - `-si` is GNU-only and errors as "illegal option -- s" under
|
||||
# BSD/macOS sed. Avoid -i altogether and do the in-place edit via a temp
|
||||
# file + mv instead, which behaves identically with either sed.
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp "${ENV_FILE}.XXXXXX")"
|
||||
sed "s/^UNIQUE_ID=.*$/UNIQUE_ID=${UNIQUE_ID}/" "$ENV_FILE" > "$tmp_file"
|
||||
mv "$tmp_file" "$ENV_FILE"
|
||||
|
||||
set_env
|
||||
}
|
||||
|
||||
function ready_repo() {
|
||||
echo "👍 Getting the repo ready to build..."
|
||||
docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
|
||||
set -euo pipefail # Recommended for robustness
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
if [[ ! -d "build" ]]; then
|
||||
git clone -b rockylinux9-x64 https://github.com/IfcOpenShell/build-outputs.git build
|
||||
else
|
||||
cd build
|
||||
git pull
|
||||
cd ..
|
||||
fi
|
||||
|
||||
if [[ ! -d "build/Linux/x86_64/install/boost-1.86.0/" ]]; then
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
cd ..
|
||||
fi
|
||||
'
|
||||
}
|
||||
|
||||
function build() {
|
||||
echo "☕ Execute the build, go make yourself a cuppa... I'll be a while"
|
||||
local BUILD_TARGET="$1"
|
||||
|
||||
docker exec -i -w "${WORKDIR}" -e PY_TGT="${PY_TGT}" -e BUILD_TARGET="${BUILD_TARGET}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v ${PY_TGT:+-$PY_TGT} --diskcleanup ${BUILD_TARGET} 2>&1 | tee build.log
|
||||
'
|
||||
echo "🎒 Pack Dependencies"
|
||||
docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
'
|
||||
|
||||
echo "🎁 Package .zip archives"
|
||||
docker exec -i -w "${WORKDIR}" -e GITHUB_SHA="$(git rev-parse HEAD)" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
|
||||
OUTPUT_DIR=${PWD}/output
|
||||
VERSION=v`cat VERSION`
|
||||
mkdir -p ${OUTPUT_DIR}
|
||||
cd ./build/`uname`/*/install/ifcopenshell
|
||||
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
numbers=`echo $py_version | grep -oE "[0-9]+\.[0-9]+" | tr -d "."`
|
||||
py_version_major=python-${numbers}$postfix
|
||||
pushd . > /dev/null
|
||||
cd $py_version
|
||||
if [ ! -d ifcopenshell ]; then
|
||||
mkdir ../ifcopenshell_
|
||||
mv * ../ifcopenshell_
|
||||
mv ../ifcopenshell_ ifcopenshell
|
||||
fi
|
||||
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
|
||||
find ifcopenshell -name "*.pyc" -delete
|
||||
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/*
|
||||
mv *.zip ${OUTPUT_DIR}/
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
cd bin
|
||||
if compgen -G "./*.zip" > /dev/null; then
|
||||
rm *.zip 2>&1 >/dev/null || true
|
||||
ls | while read exe; do
|
||||
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe
|
||||
done
|
||||
mv *.zip ${OUTPUT_DIR}/
|
||||
cd ..
|
||||
'
|
||||
}
|
||||
|
||||
function attach() {
|
||||
echo "🔦 Connect to interactive shell"
|
||||
docker exec -it -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" /bin/bash
|
||||
}
|
||||
|
||||
function try() {
|
||||
# Copies the freshly built wrapper into your actual Blender/Bonsai
|
||||
# installation for manual, in-Blender testing. This is a human-only
|
||||
# convenience: it overwrites files in your live Blender setup, so it's
|
||||
# not something that should run unattended as part of an automated or
|
||||
# AI-driven build/test loop (which should instead copy the wrapper into
|
||||
# the repo's own src/ifcopenshell-python/ifcopenshell/ - see SKILL.md).
|
||||
echo "🚴 Copying build artifacts into your Blender resource folder for testing"
|
||||
|
||||
if [[ -z "${BLENDER_USER_RESOURCE:-}" ]]; then
|
||||
echo "❌ BLENDER_USER_RESOURCE is not set in .env."
|
||||
echo " Add a line pointing at wherever Blender's user resource folder for"
|
||||
echo " the Bonsai extension actually is on your system, e.g.:"
|
||||
echo " BLENDER_USER_RESOURCE=~/.config/blender/bonsai/"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Normalise: expand a leading ~ (in case it was quoted in .env and so
|
||||
# never went through shell tilde-expansion when set_env sourced it),
|
||||
# then resolve to an absolute, symlink-free path.
|
||||
local resource="${BLENDER_USER_RESOURCE/#\~/$HOME}"
|
||||
resource="$(realpath -m "$resource")"
|
||||
|
||||
local install_dir="../build/Linux/x86_64/install"
|
||||
local py_dirs=("$install_dir"/python-*)
|
||||
if [[ ${#py_dirs[@]} -gt 1 && -n "${PY_TGT:-}" ]]; then
|
||||
# PY_TGT is compact (py-311); the install dirs are dotted
|
||||
# (python-3.11.8) - reinsert the dot (assumes a single-digit major
|
||||
# version, true for the Python 3.x line) before matching.
|
||||
local py_tgt_digits="${PY_TGT#py-}"
|
||||
local py_tgt_dotted="${py_tgt_digits:0:1}.${py_tgt_digits:1}"
|
||||
local filtered=() d
|
||||
for d in "${py_dirs[@]}"; do
|
||||
[[ "$(basename "$d")" == "python-${py_tgt_dotted}."* ]] && filtered+=("$d")
|
||||
done
|
||||
[[ ${#filtered[@]} -gt 0 ]] && py_dirs=("${filtered[@]}")
|
||||
fi
|
||||
if [[ ${#py_dirs[@]} -ne 1 || ! -d "${py_dirs[0]}" ]]; then
|
||||
echo "❌ Expected exactly one built python-* dir under $install_dir, found ${#py_dirs[@]}."
|
||||
echo " Run 'build' first, or set PY_TGT in .env to disambiguate a multi-version build."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local py_minor
|
||||
py_minor="$(basename "${py_dirs[0]}" | grep -oE '[0-9]+\.[0-9]+')"
|
||||
local wrapper_dir="${py_dirs[0]}/lib/python${py_minor}/site-packages/ifcopenshell"
|
||||
if [[ ! -f "$wrapper_dir/ifcopenshell_wrapper.py" ]]; then
|
||||
echo "❌ Built wrapper not found at $wrapper_dir - run 'build' first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local target="$resource/extensions/.local/lib/python${py_minor}/site-packages/ifcopenshell"
|
||||
mkdir -p "$target"
|
||||
cp "$wrapper_dir"/_ifcopenshell_wrapper*.so "$target/"
|
||||
cp "$wrapper_dir"/ifcopenshell_wrapper.py "$target/"
|
||||
echo "✅ Copied wrapper into $target"
|
||||
}
|
||||
|
||||
function clean() {
|
||||
# Host-side only - doesn't touch the container, image, or ccache volume.
|
||||
echo "💎 Clean the build and output folder up"
|
||||
if [[ -d "../build" ]]; then
|
||||
rm -rf ../build
|
||||
fi
|
||||
if [[ -d "../output" ]]; then
|
||||
rm -rf ../output
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
function help() {
|
||||
cat <<EOF
|
||||
Usage: ./$SCRIPT_NAME <command>
|
||||
|
||||
Available commands:
|
||||
create Build the rocky9-based image
|
||||
update Rebuild the image fresh, picking up OS package updates
|
||||
up Create the container if it doesn't exist yet, and start it
|
||||
down Remove the container entirely (docker compose down)
|
||||
stop Stop the container without removing it
|
||||
start Start a previously-stopped container
|
||||
restart stop, then start (same container, no recreation)
|
||||
recreate down, then up (fresh container)
|
||||
build Execute the IfcOpenShell build
|
||||
attach Connect to an interactive shell in the container
|
||||
try Copy the built wrapper into your Blender resource folder
|
||||
(human-only - see BLENDER_USER_RESOURCE below, and SKILL.md
|
||||
for the AI/automated-testing equivalent)
|
||||
clean Remove the build and output folders
|
||||
logs Follow container logs
|
||||
ps Show running containers
|
||||
config Validate and show compose config
|
||||
remove Remove stopped containers (docker compose rm)
|
||||
help Show this help
|
||||
|
||||
Environment variables from .env are automatically loaded, including:
|
||||
PY_TGT Restrict the build to one Python version, e.g. py-311
|
||||
UNIQUE_ID Recalculated automatically on every 'up', don't set by hand
|
||||
BLENDER_USER_RESOURCE Where 'try' copies the wrapper for manual testing, e.g.
|
||||
~/.config/blender/bonsai/
|
||||
EOF
|
||||
}
|
||||
|
||||
# ================= MAIN =================
|
||||
|
||||
case "$1" in
|
||||
create) create ;;
|
||||
update) update ;;
|
||||
up) up "${@:2}" ;;
|
||||
down) down "${@:2}" ;;
|
||||
stop) stop "${@:2}" ;;
|
||||
start) start "${@:2}" ;;
|
||||
restart) restart ;;
|
||||
recreate) recreate ;;
|
||||
build) build "${@:2}" ;;
|
||||
attach) attach ;;
|
||||
try) try ;;
|
||||
clean) clean ;;
|
||||
logs) logs "${@:2}" ;;
|
||||
ps) ps ;;
|
||||
config) config ;;
|
||||
remove) remove ;;
|
||||
help|-h|--help) help ;;
|
||||
"")
|
||||
echo "❌ No command provided."
|
||||
help
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown command: $1"
|
||||
echo "Type './$SCRIPT_NAME help' for available commands."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
+16
-30
@@ -1,6 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
# /// script
|
||||
# ///
|
||||
###############################################################################
|
||||
# #
|
||||
# This file is part of IfcOpenShell. #
|
||||
@@ -50,7 +48,7 @@ Used environment variables:
|
||||
- ``NO_CLEAN`` - do not clean `ifcopenshell` build directories but continue working on current build
|
||||
(installed dependencies are never cleared).
|
||||
By default option is disabled, to enable pass any value from `1`, `on`, `true`.
|
||||
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (8 schemas), to be supplied as `2x3;4;4x3_add2`
|
||||
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (IFC2X3; IFC4; IFC4X3_ADD2) - to be supplied as `2x3;4`
|
||||
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
|
||||
(`true` by default, any other value is considered `false`)
|
||||
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
|
||||
@@ -126,9 +124,16 @@ ssl._create_default_https_context = ssl._create_unverified_context
|
||||
import time
|
||||
from collections.abc import Generator, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Literal, Union
|
||||
from urllib.request import urlretrieve
|
||||
|
||||
try:
|
||||
from typing import Literal, Union
|
||||
except:
|
||||
# python 3.6 compatibility for rocky 8
|
||||
from typing import Union
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
ch = logging.StreamHandler()
|
||||
@@ -155,7 +160,7 @@ MPFR_VERSION = "3.1.6" # latest is 4.1.0
|
||||
CGAL_VERSION = "v5.6.3"
|
||||
USD_VERSION = "23.05"
|
||||
TBB_VERSION = "2021.9.0"
|
||||
ROCKSDB_VERSION = "10.4.2"
|
||||
ROCKSDB_VERSION = "9.11.2"
|
||||
ZSTD_VERSION = "1.5.7"
|
||||
# binaries
|
||||
cp = "cp"
|
||||
@@ -627,10 +632,9 @@ def build_dependency(
|
||||
build_tool_args: "list[str]",
|
||||
download_url: str,
|
||||
download_name: str,
|
||||
*,
|
||||
download_tool: Literal["py", "git"] = download_tool_default,
|
||||
revision: "Union[str, None]" = None,
|
||||
patch: list[str] | None = None,
|
||||
patch: "Union[str, list[str], None]" = None,
|
||||
shell=None,
|
||||
pre_compile_subs: "Sequence[tuple[str, str, str]]" = (),
|
||||
additional_files: "Union[dict[str, str], None]" = None,
|
||||
@@ -715,6 +719,8 @@ def build_dependency(
|
||||
urlretrieve(url, os.path.join(extract_dir, path))
|
||||
|
||||
if patch is not None:
|
||||
if isinstance(patch, str):
|
||||
patch = [patch]
|
||||
for p in patch:
|
||||
patch_abs = (SCRIPT_PATH / p).absolute().__str__()
|
||||
if os.path.exists(patch_abs):
|
||||
@@ -723,8 +729,6 @@ def build_dependency(
|
||||
except Exception as e:
|
||||
# Assert that the patch has already been applied
|
||||
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
|
||||
else:
|
||||
raise FileNotFoundError(patch_abs)
|
||||
|
||||
if shell is not None:
|
||||
sp.run(shell, shell=True, check=True, cwd=extract_dir)
|
||||
@@ -1090,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
|
||||
@@ -1172,14 +1167,6 @@ if "cgal" in targets:
|
||||
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
|
||||
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
|
||||
|
||||
# Fixes configure failing to find a working compiler under GCC 15's default -std=gnu23.
|
||||
# Issue presumably will be resolved in any next gmp version, but currently the last one is 6.3.0.
|
||||
# Patch is just applying fix from upstream meantion below:
|
||||
# https://gmplib.org/list-archives/gmp-bugs/2025-February/005561.html
|
||||
gmp_patches = ["./patches/gmp/001-fix-std23.patch"]
|
||||
if GMP_VERSION != "6.3.0":
|
||||
raise Exception(f"GMP_VERSION changed to {GMP_VERSION}, check whether {gmp_patches} is still needed.")
|
||||
|
||||
build_dependency(
|
||||
name=f"gmp-{GMP_VERSION}",
|
||||
mode="autoconf",
|
||||
@@ -1187,7 +1174,6 @@ if "cgal" in targets:
|
||||
pre_compile_subs=(
|
||||
[("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []
|
||||
),
|
||||
patch=gmp_patches,
|
||||
# Sometimes ftp.gnu.org is very slow, use ftpmirror.gnu.org as a workaround.
|
||||
download_url="https://ftpmirror.gnu.org/gnu/gmp/",
|
||||
download_name=f"gmp-{GMP_VERSION}.tar.bz2",
|
||||
@@ -1529,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("")
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# /// script
|
||||
# ///
|
||||
"""
|
||||
Cache built dependencies for builds.
|
||||
|
||||
@@ -43,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}'")
|
||||
@@ -68,7 +63,6 @@ def unpack_dependencies(install_dir: Path) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
action = None
|
||||
if len(sys.argv) != 2 or (action := sys.argv[1].lower()) not in ("pack", "unpack"):
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
Fixes configure failing to find a working compiler under GCC 15's default
|
||||
-std=gnu23 (upstream fix: https://gmplib.org/repo/gmp/rev/8e7bb4ae7a18).
|
||||
|
||||
Upstream fix is patching `acinclude.m4`, but since in the release tarball
|
||||
all macros are already expanded to `configure` script, so we're patching
|
||||
all occurrences of that macro.
|
||||
|
||||
--- a/configure
|
||||
+++ b/configure
|
||||
@@ -6568,7 +6568,7 @@
|
||||
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
@@ -8187,7 +8187,7 @@
|
||||
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
@@ -0,0 +1,32 @@
|
||||
http://git.dev.opencascade.org/gitweb/?p=occt.git;a=commitdiff;h=0ab4e621833f4eae945a3762c9a29ee12e2eec53#patch1
|
||||
diff --git a/src/HLRBRep/HLRBRep_InternalAlgo.cxx b/src/HLRBRep/HLRBRep_InternalAlgo.cxx
|
||||
index ca885ca..c13cb06 100644 (file)
|
||||
--- a/src/HLRBRep/HLRBRep_InternalAlgo.cxx
|
||||
+++ b/src/HLRBRep/HLRBRep_InternalAlgo.cxx
|
||||
@@ -165,7 +165,7 @@ void HLRBRep_InternalAlgo::Update ()
|
||||
SB.Bounds(v1,v2,e1,e2,f1,f2);
|
||||
|
||||
for (Standard_Integer e = e1; e <= e2; e++) {
|
||||
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
|
||||
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
|
||||
HLRAlgo::DecodeMinMax(ed.MinMax(), TheMin, TheMax);
|
||||
if (FirstTime) {
|
||||
FirstTime = Standard_False;
|
||||
@@ -307,7 +307,7 @@ void HLRBRep_InternalAlgo::InitEdgeStatus ()
|
||||
Standard_Integer nf = myDS->NbFaces();
|
||||
|
||||
for (Standard_Integer e = 1; e <= ne; e++) {
|
||||
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
|
||||
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
|
||||
if (ed.Selected()) ed.Status().ShowAll();
|
||||
}
|
||||
// for (Standard_Integer f = 1; f <= nf; f++) {
|
||||
@@ -368,7 +368,7 @@ void HLRBRep_InternalAlgo::Select ()
|
||||
Standard_Integer nf = myDS->NbFaces();
|
||||
|
||||
for (Standard_Integer e = 1; e <= ne; e++) {
|
||||
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
|
||||
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
|
||||
ed.Selected(Standard_True);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
From a0deb4ce8b43cf3c8b8c0a4225c6be5296446dbd Mon Sep 17 00:00:00 2001
|
||||
From: Adam Eri <adam.eri@blackmirror.media>
|
||||
Date: Tue, 3 Sep 2019 23:30:20 +0200
|
||||
Subject: [PATCH] Resolves compile error on macOS
|
||||
|
||||
Resolves "no member named 'isnan' in namespace 'std'" on macOS
|
||||
---
|
||||
GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
|
||||
diff --git a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
|
||||
index 1f9a3eef..dd6f5c59 100644
|
||||
--- a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
|
||||
+++ b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "GeneratedSaxParserUtils.h"
|
||||
#include <math.h>
|
||||
+#include <cmath>
|
||||
#include <memory>
|
||||
#include <string.h>
|
||||
#include <limits>
|
||||
+12
-11
@@ -1,11 +1,6 @@
|
||||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
PYODIDE_VERSION=0.29.3
|
||||
PYODIDE_BUILD_VERSION=0.33.0
|
||||
PYODIDE_XBUILDENV_ROOT="${HOME}/.cache/.pyodide-xbuildenv-${PYODIDE_BUILD_VERSION}"
|
||||
PYODIDE_XBUILDENV="${PYODIDE_XBUILDENV_ROOT}/${PYODIDE_VERSION}"
|
||||
|
||||
# Script is assuming that it will be possible to execute it multiple times
|
||||
# therefore we're clearing venv each time and ignoring existing 'emsdk' folder.
|
||||
|
||||
@@ -16,15 +11,21 @@ source .venv/bin/activate
|
||||
|
||||
# Install pyodide cross build environment.
|
||||
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
|
||||
uv pip install "pyodide-build==${PYODIDE_BUILD_VERSION}"
|
||||
uv pip install pyodide-build
|
||||
# `uv run` is required, so xbuildenv would skip using `pip`.
|
||||
uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
|
||||
uv run pyodide xbuildenv install-emscripten
|
||||
uv run pyodide xbuildenv install
|
||||
|
||||
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
|
||||
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
|
||||
emcc --version
|
||||
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},
|
||||
)
|
||||
|
||||
+11
-135
@@ -1,8 +1,12 @@
|
||||
[project]
|
||||
name = "IfcOpenShell"
|
||||
version = "0.0.0"
|
||||
# Don't provide requires-python explicitly
|
||||
# allowing pyprojects to set their own (e.g. bonsai and general ifcopenshell version differ).
|
||||
dependencies = [
|
||||
"black==26.3.1",
|
||||
"ruff==0.15.5",
|
||||
"poethepoet",
|
||||
"gersemi==0.26.0",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
@@ -24,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
|
||||
@@ -38,7 +39,6 @@ exclude = [
|
||||
# then they will be inherited by projects' .toml files.
|
||||
# This allows using assuming different Python version for different projects.
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
exclude = [
|
||||
# Submodules.
|
||||
"src/ifcopenshell-python/ifcopenshell/express",
|
||||
@@ -78,139 +78,15 @@ ignore = [
|
||||
"UP032", # Replace .format with f-string
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
all = "error"
|
||||
|
||||
# Structural rules (no deep type inference needed, easier to adapt).
|
||||
# Maybe later, requires to specify element types for all generics.
|
||||
missing-type-argument = "ignore"
|
||||
# Conflicts with `bpy` props defined using annotations.
|
||||
invalid-type-form = "ignore"
|
||||
|
||||
# Non-structural rules:
|
||||
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
|
||||
call-non-callable = "ignore"
|
||||
# bpy is missing some context manager implementations.
|
||||
invalid-context-manager = "ignore"
|
||||
# Doesn't go well with `bpy.ops.xxx.yyy`.
|
||||
unresolved-attribute = "ignore"
|
||||
# Too many false positives.
|
||||
invalid-argument-type = "ignore"
|
||||
invalid-method-override = "ignore"
|
||||
invalid-assignment = "ignore"
|
||||
invalid-parameter-default = "ignore"
|
||||
missing-override-decorator = "ignore"
|
||||
invalid-yield = "ignore"
|
||||
invalid-return-type = "ignore"
|
||||
non-callable-init-subclass = "ignore"
|
||||
not-iterable = "ignore"
|
||||
possibly-missing-attribute = "ignore"
|
||||
no-matching-overload = "ignore"
|
||||
not-subscriptable = "ignore"
|
||||
unsupported-dynamic-base = "ignore"
|
||||
unsupported-operator = "ignore"
|
||||
|
||||
[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]
|
||||
|
||||
dev-setup.sequence = [
|
||||
{cmd = "uv sync"},
|
||||
{cmd = "uv pip install -e ./src/bsdd/"},
|
||||
{cmd = "uv pip install -e ./src/ifcopenshell-python/[advanced,dev]"},
|
||||
{cmd = "uv pip install -e ./src/ifcedit/"},
|
||||
{cmd = "uv pip install -e ./src/ifcpatch/"},
|
||||
{cmd = "uv pip install -e ./src/ifcquery/"},
|
||||
{cmd = "uv pip install -e './src/ifcmcp/[mcp]'"},
|
||||
{cmd = "uv pip install -r src/bonsai/requirements-dev.txt"},
|
||||
]
|
||||
dev-setup.help = "Install repo packages in editable mode"
|
||||
|
||||
ruff = "ruff check"
|
||||
ruff-main = "ruff check --extend-exclude nix/build-all.py"
|
||||
# It's actually Python 3.6, but ruff only supports 3.7+, but it should do.
|
||||
ruff-old = "ruff check nix/build-all.py --target-version py37"
|
||||
ruff.sequence = ["ruff-main", "ruff-old"]
|
||||
|
||||
black = "black ."
|
||||
|
||||
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"]
|
||||
format.sequence = ["black", "ruff-main", "ruff-old"]
|
||||
|
||||
cmake-format = "gersemi . --in-place"
|
||||
|
||||
[tool.poe.tasks.ty-ios]
|
||||
cmd = """
|
||||
ty check
|
||||
nix/
|
||||
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
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.bonsai-deps]
|
||||
help = "Clone or update Bonsai external dependencies."
|
||||
cmd = "python src/bonsai/scripts/bonsai_deps.py"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
black==26.3.1
|
||||
ruff==0.15.22
|
||||
poethepoet
|
||||
ty==0.0.61
|
||||
gersemi==0.26.1
|
||||
@@ -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()
|
||||
@@ -188,8 +188,6 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
@@ -208,8 +206,6 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
@@ -226,8 +222,6 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
@@ -261,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",
|
||||
)
|
||||
|
||||
+9
-18
@@ -17,8 +17,8 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
SHELL := sh
|
||||
PYTHON:=python3
|
||||
PIP:=pip3
|
||||
PYTHON:=python3.11
|
||||
PIP:=pip3.11
|
||||
PATCH:=patch
|
||||
SED:=sed -i
|
||||
VENV_ACTIVATE:=bin/activate
|
||||
@@ -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
|
||||
@@ -106,7 +104,7 @@ endif
|
||||
endif # def PLATFORM
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=3e7b739
|
||||
OLD:=1c5b825
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -192,11 +190,7 @@ endif
|
||||
# Provides networkx graph analysis for project dependency calculations
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download networkx --dest=./wheels
|
||||
# Required by IFCDiff
|
||||
# Pinned <9.1: deepdiff 9.1.0 adds cachebox<6,>=5.2 which only ships macOS x86_64
|
||||
# wheels for macosx_10_12+ and is incompatible with our macos py311 --platform
|
||||
# macosx_10_10_x86_64 target. Revisit once the macos py311 platform tag is bumped
|
||||
# to 10_13 (matching py312/py313).
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download "deepdiff<9.1" --dest=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download deepdiff --dest=./wheels
|
||||
# Required by IFCCSV and ifcopenshell.util.selector
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lark --dest=./wheels
|
||||
# Required by IFC4D
|
||||
@@ -238,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
|
||||
@@ -266,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
|
||||
|
||||
@@ -360,10 +355,6 @@ else
|
||||
pytest test/tool/test_$(MODULE).py --maxfail=1
|
||||
endif
|
||||
|
||||
.PHONY: test-modal
|
||||
test-modal:
|
||||
blender --enable-event-simulate --python test/modal/test_modal.py --window-maximized
|
||||
|
||||
# Reregistering test is not added to the standard test suite because during unregister
|
||||
# Blender removes all Bonsai dependencies breaking dev-environment symlinks.
|
||||
.PHONY: test-reregister
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was modified with the assistance of an AI coding tool.
|
||||
|
||||
import importlib
|
||||
import os
|
||||
@@ -27,7 +25,7 @@ import bpy
|
||||
import bpy.utils.previews
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
|
||||
from . import handler, operator, parametric_lifecycle, prop, ui
|
||||
from . import handler, operator, prop, ui
|
||||
|
||||
try:
|
||||
from bonsai.translations import translations_dict
|
||||
@@ -90,7 +88,6 @@ modules = {
|
||||
"web": None,
|
||||
"light": None,
|
||||
"alignment": None,
|
||||
"clip_box": None,
|
||||
# Uncomment this line to enable loading of the demo module. Happy hacking!
|
||||
# The name "demo" must correlate to a folder name in `bim/module/`.
|
||||
# "demo": None,
|
||||
@@ -160,6 +157,9 @@ classes = [
|
||||
ui.BIM_UL_tab_visibilities,
|
||||
ui.BIM_UL_panel_visibilities,
|
||||
ui.DocPreferences,
|
||||
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
|
||||
ui.GizmoPreferencesWindow, # Register before GizmoPreferences
|
||||
ui.GizmoPreferencesStair, # Register before GizmoPreferences
|
||||
ui.GizmoPreferences,
|
||||
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
|
||||
# Tabs panel
|
||||
@@ -268,8 +268,6 @@ def register():
|
||||
bpy.app.handlers.depsgraph_update_post.append(on_register)
|
||||
bpy.app.handlers.undo_post.append(handler.undo_post)
|
||||
bpy.app.handlers.redo_post.append(handler.redo_post)
|
||||
# Must follow the two appends above so regenerators see restored IFC state.
|
||||
parametric_lifecycle.install_parametric_lifecycle_handlers()
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
bpy.app.handlers.load_post.append(handler.loadIfcStore)
|
||||
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
|
||||
@@ -327,7 +325,6 @@ def unregister():
|
||||
|
||||
unregister_classes(classes)
|
||||
|
||||
parametric_lifecycle.uninstall_parametric_lifecycle_handlers()
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
|
||||
del bpy.types.Scene.BIMProperties
|
||||
|
||||
@@ -24,28 +24,10 @@ a text, a tspan { fill: blue !important; text-decoration: underline;}
|
||||
a:hover { cursor: pointer; }
|
||||
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
|
||||
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
/* SVG edge classification (issue #3668): see edge-classification.md. These select directly on
|
||||
the <path> element (each classified projection edge carries its own class), so they win over
|
||||
the inherited .projection rule above regardless of specificity. */
|
||||
path.outline { stroke: black; stroke-width: 0.35; stroke-opacity: 1; }
|
||||
path.boundary { stroke: black; stroke-width: 0.3; stroke-opacity: 0.9; }
|
||||
path.crease { stroke: black; stroke-width: 0.25; stroke-opacity: 0.85; }
|
||||
path.sharp { stroke: black; stroke-width: 0.18; stroke-opacity: 0.7; }
|
||||
path.flush { stroke: black; stroke-width: 0.1; stroke-opacity: 0.4; }
|
||||
|
||||
/* Debug CSS for troubleshooting edge classification */
|
||||
/*
|
||||
path.outline { stroke: black; stroke-width: 0.35; stroke-opacity: 1; }
|
||||
path.boundary { stroke: orange; stroke-width: 0.3; stroke-opacity: 0.9; }
|
||||
path.crease { stroke: green; stroke-width: 0.25; stroke-opacity: 0.85; }
|
||||
path.sharp { stroke: red; stroke-width: 0.18; stroke-opacity: 0.7; }
|
||||
path.flush { stroke: blue; stroke-width: 0.1; stroke-opacity: 0.4; }
|
||||
*/
|
||||
|
||||
.surface {fill: white; stroke-width: 0.1;}
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
|
||||
/* .IfcGeographicElement { fill: none; stroke: rgb(150, 150, 150); stroke-linecap: 'round'; stroke-dasharray: 1, 2;} */
|
||||
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }
|
||||
.PredefinedType-LINEWORK { stroke: black; stroke-width: 0.25; }
|
||||
.PredefinedType-LINEWORK.dashed { stroke-dasharray: 3, 2; }
|
||||
.PredefinedType-LINEWORK.fine { stroke-width: 0.18; stroke: #777777; }
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
Copyright (c) 2011-2012, Nikita Volchenkov (<nikitavolchenkov@gmail.com>),
|
||||
with Reserved Font Name OpenGost Type B.
|
||||
|
||||
Copyright (c) 2012, Valek Filippov (<frob@gnome.org>).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_D
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2,#31,#32,#33,#34,#35,#36,#37));
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -33,14 +33,5 @@ DATA;
|
||||
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#29=IFCSIMPLEPROPERTYTEMPLATE('0lP6Y8q9v2QhDnR4sT7uVx',$,'PerspectiveShiftX','Horizontal perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#30=IFCSIMPLEPROPERTYTEMPLATE('2mR8b1NcW5EoFyG7hJ9kLp',$,'PerspectiveShiftY','Vertical perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#31=IFCSIMPLEPROPERTYTEMPLATE('1cFVJnqT13m8ItkMHaI1tp',$,'UseEdgeClassification','Enable the boundary/outline/sharp/crease/flush SVG edge classification scheme (issue #3668). When false, drawings use the original unclassified linework.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#32=IFCSIMPLEPROPERTYTEMPLATE('2kB$mxBgnBUvhjh0Ti0c4P',$,'RenderCreases','Whether to render ''crease'' (concave) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#33=IFCSIMPLEPROPERTYTEMPLATE('3MSIJNW$T8r9Hl12kk0BY$',$,'ValleyAngleMinDegrees','Minimum concave dihedral deviation from flat, in degrees, for a projection edge to be classified as ''crease'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#34=IFCSIMPLEPROPERTYTEMPLATE('2epSGfC4bFM9gb1X7zBIp4',$,'RenderSharp','Whether to render ''sharp'' (convex) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#35=IFCSIMPLEPROPERTYTEMPLATE('3TZwsEjkr5WRDKcgrYzSIA',$,'RidgeAngleMinDegrees','Minimum convex dihedral deviation from flat, in degrees, for a projection edge to be classified as ''sharp'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#36=IFCSIMPLEPROPERTYTEMPLATE('2Jua$lO754vgZOkBoHM2gA',$,'RenderFlush','Whether to render ''flush'' edges (dihedral deviation below both ridge/valley thresholds). Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#37=IFCSIMPLEPROPERTYTEMPLATE('1zM9sia2L8RQDnWZxgUwlZ',$,'JoinClasses','Comma separated list of IFC classes whose cut linework will be joined together when they meet (e.g. mitred at a corner).\X2\000A\X0\Defaults to ''IfcWall,IfcSlab'' if not set. Override to also join other classes, such as ''IfcWall,IfcSlab,IfcCovering''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -244,7 +244,7 @@ function addGanttElement(blenderId, tasks, workSched, filename) {
|
||||
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ["Hour", "Day", "Week", "Month", "Quarter"], // vUseSingleCell keeps Hour usable on large charts.
|
||||
vFormatArr: ["Day", "Week", "Month", "Quarter"], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vShowRes: true, // Disable the resource column.
|
||||
vShowComp: false, // Disable the completion column.
|
||||
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Shared structural-change cache token for POST_VIEW decorators.
|
||||
|
||||
Decorators include the token in their cache key and rebuild on bump."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
import bpy
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
_DECORATOR_CACHE_TOKEN = 0
|
||||
|
||||
|
||||
def get_decorator_cache_token() -> int:
|
||||
return _DECORATOR_CACHE_TOKEN
|
||||
|
||||
|
||||
def reset_for_test() -> None:
|
||||
"""Test-only: reset the cache token to 0 so bump-count assertions are stable."""
|
||||
global _DECORATOR_CACHE_TOKEN
|
||||
_DECORATOR_CACHE_TOKEN = 0
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def _bump_decorator_cache_token(*args: Any) -> None:
|
||||
"""depsgraph_update_post fires every animation frame and every driver
|
||||
evaluation, even when no IFC-relevant ID block changed. Unconditional
|
||||
bumping defeats the cache: an animated scene rebuilds every decorator
|
||||
every viewport tick. Gate the depsgraph path on Object geometry or
|
||||
transform updates; undo / redo / load have no depsgraph and always
|
||||
invalidate.
|
||||
|
||||
Coverage assumption: ``TokenCache`` consumers key on Object identity
|
||||
(depsgraph updates whose ``id`` is a ``bpy.types.Object``). Mesh /
|
||||
Material / NodeTree updates that don't surface as an Object change
|
||||
do NOT invalidate the token — a decorator that caches material- or
|
||||
mesh-data-derived state must gate on a separate signal."""
|
||||
global _DECORATOR_CACHE_TOKEN
|
||||
if len(args) >= 2:
|
||||
depsgraph = args[1]
|
||||
if depsgraph is not None and hasattr(depsgraph, "updates"):
|
||||
if not any(
|
||||
(getattr(u, "is_updated_geometry", False) or getattr(u, "is_updated_transform", False))
|
||||
and hasattr(u, "id")
|
||||
and isinstance(u.id, bpy.types.Object)
|
||||
for u in depsgraph.updates
|
||||
):
|
||||
return
|
||||
_DECORATOR_CACHE_TOKEN += 1
|
||||
|
||||
|
||||
def _hooks() -> tuple[Any, ...]:
|
||||
return (
|
||||
bpy.app.handlers.depsgraph_update_post,
|
||||
bpy.app.handlers.undo_post,
|
||||
bpy.app.handlers.redo_post,
|
||||
bpy.app.handlers.load_post,
|
||||
)
|
||||
|
||||
|
||||
def install_decorator_cache_handlers() -> None:
|
||||
"""Append the bump handler to each hook; idempotent."""
|
||||
for hook in _hooks():
|
||||
if _bump_decorator_cache_token not in hook:
|
||||
hook.append(_bump_decorator_cache_token)
|
||||
|
||||
|
||||
def uninstall_decorator_cache_handlers() -> None:
|
||||
for hook in _hooks():
|
||||
try:
|
||||
hook.remove(_bump_decorator_cache_token)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class TokenCache(Generic[T]):
|
||||
"""Memoise a single value keyed on ``(caller_key, get_decorator_cache_token())``.
|
||||
|
||||
The token component invalidates the cache on depsgraph / undo / redo / load,
|
||||
so cached ``bpy.types.Object`` references can't outlive the underlying ID
|
||||
blocks. Holds exactly one entry — last key wins."""
|
||||
|
||||
__slots__ = ("_key", "_value")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._key: tuple[Any, int] | None = None
|
||||
self._value: T | None = None
|
||||
|
||||
def get_or_compute(self, key: Any, compute: Callable[[], T]) -> T:
|
||||
token_key = (key, _DECORATOR_CACHE_TOKEN)
|
||||
if token_key == self._key:
|
||||
return self._value # type: ignore[return-value]
|
||||
value = compute()
|
||||
self._key = token_key
|
||||
self._value = value
|
||||
return value
|
||||
@@ -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()
|
||||
|
||||
@@ -15,12 +15,11 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was modified with the assistance of an AI coding tool.
|
||||
|
||||
import os
|
||||
import weakref
|
||||
from collections.abc import Callable
|
||||
from math import cos
|
||||
from typing import Union
|
||||
|
||||
import bpy
|
||||
@@ -32,49 +31,30 @@ from bpy.app.handlers import persistent
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.bim
|
||||
import bonsai.core.model as core_model
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.decorator_cache import (
|
||||
install_decorator_cache_handlers,
|
||||
uninstall_decorator_cache_handlers,
|
||||
)
|
||||
from bonsai.bim.ifc import IfcStore, get_cache_or_detect_lock
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
|
||||
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
|
||||
from bonsai.bim.module.model.array import (
|
||||
ArrayPreviewDecorator,
|
||||
ArraySelectionHighlightDecorator,
|
||||
)
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bonsai.bim.module.model.decorator import (
|
||||
BendPreviewDecorator,
|
||||
BoundingBoxDecorator,
|
||||
DoorSwingReadonlyDecorator,
|
||||
MEPSegmentExtendPreviewDecorator,
|
||||
MEPSystemPathDecorator,
|
||||
SlabDirectionDecorator,
|
||||
WallAxisDecorator,
|
||||
WallFilletPreviewDecorator,
|
||||
WallSystemPathDecorator,
|
||||
)
|
||||
from bonsai.bim.module.model.wall import WallGizmoPreviewDecorator
|
||||
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
|
||||
@@ -125,13 +105,19 @@ def active_object_callback():
|
||||
|
||||
|
||||
def update_bim_tool_props():
|
||||
"""Selection-driven BIM Tool sync: re-target user-intent enums
|
||||
(ifc_class, relating_type_id) AND refresh header values
|
||||
(extrusion_depth, length, x_angle) for the new active object."""
|
||||
ctx = _resolve_bim_tool_context()
|
||||
if ctx is None:
|
||||
"""update BIM Tools props (such as extrusion_depth, length and x_angle) when active object changes"""
|
||||
obj = bpy.context.active_object
|
||||
|
||||
# bunch of checks to see if we're in a valid state
|
||||
if not obj:
|
||||
return
|
||||
mode = bpy.context.mode
|
||||
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode)
|
||||
if not current_tool or current_tool.idname not in tool.Blender.get_list_of_tools():
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
obj, current_tool, element = ctx
|
||||
|
||||
props = tool.Model.get_model_props()
|
||||
aprops = tool.Drawing.get_annotation_props()
|
||||
@@ -144,85 +130,18 @@ def update_bim_tool_props():
|
||||
|
||||
if is_annotation_tool and (object_type := tool.Drawing.get_annotation_type_object_type(element_type)):
|
||||
aprops.object_type = object_type
|
||||
try:
|
||||
aprops.relating_type_id = str(element_type.id())
|
||||
except TypeError:
|
||||
# EnumProperty items are rebuilt asynchronously when ifc_class changes;
|
||||
# this assignment can race a stale item list. Skipping is harmless —
|
||||
# the UI will resync on the next active_object_callback.
|
||||
pass
|
||||
aprops.relating_type_id = str(element_type.id())
|
||||
return
|
||||
|
||||
if is_bim_tool:
|
||||
try:
|
||||
props.ifc_class = element_type.is_a()
|
||||
except TypeError:
|
||||
# ifc_class only lists element/space types present in the model, so an
|
||||
# unsupported type (e.g. a raw IfcTypeProduct) or a stale item list mid-
|
||||
# rebuild raises `enum "<class>" not found`. Skip rather than crash the
|
||||
# handler — it re-fires on the next selection and the panel resyncs.
|
||||
pass
|
||||
props.ifc_class = element_type.is_a()
|
||||
|
||||
# Only assign when the target enum is the one that lists this type — otherwise
|
||||
# we hit `enum "<id>" not found in (...)` if the user selects an element of a
|
||||
# different class than the workspace tool was built for (e.g. selecting a wall
|
||||
# while the door tool is active).
|
||||
tool_class_match = TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a()
|
||||
bim_tool_class_match = is_bim_tool and props.ifc_class == element_type.is_a()
|
||||
if bim_tool_class_match or tool_class_match:
|
||||
try:
|
||||
props.relating_type_id = str(element_type.id())
|
||||
except TypeError:
|
||||
# Defensive: the enum item list can lag behind ifc_class assignment
|
||||
# above. Skipping leaves the panel briefly out of sync rather than
|
||||
# crashing the handler (which Blender re-fires on every selection).
|
||||
pass
|
||||
if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a():
|
||||
props.relating_type_id = str(element_type.id())
|
||||
|
||||
if is_annotation_tool:
|
||||
return
|
||||
|
||||
_read_headers_into_props(obj, element)
|
||||
|
||||
|
||||
def refresh_bim_tool_headers():
|
||||
"""Push the active IFC entity's current header float values
|
||||
(extrusion_depth, length, x_angle) into ``BIMModelProperties``.
|
||||
Enum-safe: never writes user-intent enum slots, which are owned by
|
||||
the selection callback."""
|
||||
ctx = _resolve_bim_tool_context()
|
||||
if ctx is None:
|
||||
return
|
||||
obj, current_tool, element = ctx
|
||||
if current_tool.idname not in tool.Blender.get_property_header_tools():
|
||||
return
|
||||
_read_headers_into_props(obj, element)
|
||||
|
||||
|
||||
def _resolve_bim_tool_context():
|
||||
"""Return ``(obj, current_tool, element)`` when an active BIM workspace
|
||||
tool sees a resolvable IFC element; ``None`` otherwise. Defensive
|
||||
against stripped operator contexts — a missing ``active_object`` /
|
||||
``mode`` / ``workspace`` short-circuits to ``None`` instead of raising."""
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return None
|
||||
mode = getattr(bpy.context, "mode", None)
|
||||
workspace = getattr(bpy.context, "workspace", None)
|
||||
if mode is None or workspace is None:
|
||||
return None
|
||||
current_tool = workspace.tools.from_space_view3d_mode(mode)
|
||||
if not current_tool or current_tool.idname not in tool.Blender.get_list_of_tools():
|
||||
return None
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return None
|
||||
return obj, current_tool, element
|
||||
|
||||
|
||||
def _read_headers_into_props(obj, element):
|
||||
"""Populate ``BIMModelProperties`` header values from the active
|
||||
object's IFC extrusion. Enum-safe: writes only header floats, never
|
||||
user-intent enum slots, so it is safe to call on the post-commit hook."""
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
@@ -240,13 +159,10 @@ def _read_headers_into_props(obj, element):
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
|
||||
props = tool.Model.get_model_props()
|
||||
if AuthoringData.data["active_material_usage"] == "LAYER2":
|
||||
x_angle = get_x_angle(extrusion)
|
||||
axis = tool.Model.get_wall_axis(obj)["reference"]
|
||||
props.extrusion_depth = core_model.vertical_height_from_extrusion_depth(
|
||||
extrusion.Depth * si_conversion, x_angle
|
||||
)
|
||||
props.extrusion_depth = abs(extrusion.Depth * si_conversion * cos(x_angle))
|
||||
props.length = (axis[1] - axis[0]).length
|
||||
props.x_angle = x_angle
|
||||
|
||||
@@ -273,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,
|
||||
@@ -320,11 +236,9 @@ def loadIfcStore(scene: bpy.types.Scene) -> None:
|
||||
IfcStore.purge()
|
||||
refresh_ui_data()
|
||||
if not tool.Ifc.get():
|
||||
tool.Autosave.cancel_timer()
|
||||
return
|
||||
tool.Ifc.schema()
|
||||
IfcStore.relink_all_objects()
|
||||
tool.Autosave.reset_timer()
|
||||
|
||||
|
||||
@persistent
|
||||
@@ -439,10 +353,8 @@ def subscribe_to_viewport_shading_changes():
|
||||
)
|
||||
|
||||
|
||||
def _apply_save_file_invariants(scene: bpy.types.Scene) -> None:
|
||||
"""Invariants enforced on every load_post: msgbus subscription, IFC owner
|
||||
settings, scene-bound caches, load-transient parametric state, and the
|
||||
multi-instance lock probe."""
|
||||
@persistent
|
||||
def load_post(scene):
|
||||
global global_subscription_owner
|
||||
active_object_key = bpy.types.LayerObjects, "active"
|
||||
bpy.msgbus.subscribe_rna(
|
||||
@@ -453,23 +365,6 @@ def _apply_save_file_invariants(scene: bpy.types.Scene) -> None:
|
||||
ifcopenshell.api.owner.settings.get_application = get_application
|
||||
AuthoringData.type_thumbnails = {}
|
||||
|
||||
tool.Parametric.on_load_post(scene)
|
||||
|
||||
if tool.Ifc.get() and bpy.data.is_saved:
|
||||
props = tool.Blender.get_bim_props()
|
||||
props.has_blend_warning = True
|
||||
|
||||
# Probe the H5 cooked-geometry cache so the multi-instance warning surfaces
|
||||
# right after .blend load. Without this, the lock is only detected when a
|
||||
# mutation triggers ``clear_cache`` — by which time the user has already
|
||||
# made changes that may now conflict with the other Blender instance.
|
||||
if tool.Ifc.get():
|
||||
get_cache_or_detect_lock()
|
||||
|
||||
|
||||
def _apply_user_preferences() -> None:
|
||||
"""User-preference-driven UI setup: toolbar, BIM workspace, viewport shading
|
||||
subscription, scene-panel hijack, tab layout, snap defaults."""
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
if not preferences.should_setup_toolbar:
|
||||
tool.Blender.unregister_toolbar()
|
||||
@@ -493,21 +388,11 @@ def _apply_user_preferences() -> None:
|
||||
tool.Blender.override_scene_panel(panel)
|
||||
tool.Blender.setup_tabs()
|
||||
|
||||
if preferences.should_use_snap and (scene := bpy.context.scene):
|
||||
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
|
||||
scene.tool_settings.use_snap = True
|
||||
# Match default Bonsai snaps
|
||||
scene.tool_settings.snap_elements_base = {"EDGE", "EDGE_PERPENDICULAR", "VERTEX", "EDGE_MIDPOINT", "FACE"}
|
||||
if tool.Ifc.get() and bpy.data.is_saved:
|
||||
props = tool.Blender.get_bim_props()
|
||||
props.has_blend_warning = True
|
||||
|
||||
tool.Blender.sync_old_preferences()
|
||||
|
||||
|
||||
def _install_viewport_overlays() -> None:
|
||||
"""Sync every Bonsai viewport decorator to its enabled state.
|
||||
|
||||
Wrapped in uninstall/install of the decorator-cache bump handlers so a
|
||||
decorator's own install path doesn't double-bind to depsgraph_update_post
|
||||
via ``TokenCache`` instances created during their own ``install()``."""
|
||||
# Bonsai overlays
|
||||
georeference_props = tool.Georeference.get_georeference_props()
|
||||
aggregate_props = tool.Aggregate.get_aggregate_props()
|
||||
nest_props = tool.Nest.get_nest_props()
|
||||
@@ -517,62 +402,23 @@ def _install_viewport_overlays() -> None:
|
||||
NestDecorator.uninstall()
|
||||
WallAxisDecorator.uninstall()
|
||||
SlabDirectionDecorator.uninstall()
|
||||
MEPSystemPathDecorator.uninstall()
|
||||
WallSystemPathDecorator.uninstall()
|
||||
WallFilletPreviewDecorator.uninstall()
|
||||
BendPreviewDecorator.uninstall()
|
||||
MEPSegmentExtendPreviewDecorator.uninstall()
|
||||
WallGizmoPreviewDecorator.uninstall()
|
||||
DoorSwingReadonlyDecorator.uninstall()
|
||||
ArrayPreviewDecorator.uninstall()
|
||||
ArraySelectionHighlightDecorator.uninstall()
|
||||
uninstall_decorator_cache_handlers()
|
||||
try:
|
||||
if georeference_props.should_visualise:
|
||||
GeoreferenceDecorator.install(bpy.context)
|
||||
if aggregate_props.aggregate_decorator:
|
||||
AggregateDecorator.install(bpy.context)
|
||||
if nest_props.nest_decorator:
|
||||
NestDecorator.install(bpy.context)
|
||||
if model_props.show_wall_axis:
|
||||
WallAxisDecorator.install(bpy.context)
|
||||
if model_props.show_slab_direction:
|
||||
SlabDirectionDecorator.install(bpy.context)
|
||||
if model_props.show_paths:
|
||||
MEPSystemPathDecorator.install(bpy.context)
|
||||
WallSystemPathDecorator.install(bpy.context)
|
||||
if model_props.show_bounding_box:
|
||||
BoundingBoxDecorator.install(bpy.context)
|
||||
# Always-installed: draw() self-polls on Scene.BIMPreviewProperties.
|
||||
# wall_fillet.is_active, so installation has no cost when no preview
|
||||
# is open. No corresponding addon-preference toggle.
|
||||
WallFilletPreviewDecorator.install(bpy.context)
|
||||
# Always-installed siblings of WallFilletPreviewDecorator: each
|
||||
# self-polls on its own scene.BIMPreviewProperties subgroup or on
|
||||
# selection + hover gizmo state — zero cost when nothing is active.
|
||||
BendPreviewDecorator.install(bpy.context)
|
||||
MEPSegmentExtendPreviewDecorator.install(bpy.context)
|
||||
# Always-installed: draw_lines() self-polls on selection + hover state
|
||||
# for join / extend-to-wall / cursor-extend / cursor-split previews.
|
||||
# Free when no preview-eligible state is active.
|
||||
WallGizmoPreviewDecorator.install(bpy.context)
|
||||
# Always-installed: draw() self-polls on active object + IfcDoor +
|
||||
# parametric pset, so the cost is one bpy/IFC lookup per redraw when
|
||||
# nothing eligible is selected.
|
||||
DoorSwingReadonlyDecorator.install(bpy.context)
|
||||
# Always-installed: draw() self-polls on the active object's array
|
||||
# family membership, so installation has no cost when no array
|
||||
# element is selected.
|
||||
ArraySelectionHighlightDecorator.install(bpy.context)
|
||||
# Always-installed: draw() self-polls on props.is_editing — only
|
||||
# paints during an active array edit lifecycle.
|
||||
ArrayPreviewDecorator.install(bpy.context)
|
||||
finally:
|
||||
install_decorator_cache_handlers()
|
||||
if georeference_props.should_visualise:
|
||||
GeoreferenceDecorator.install(bpy.context)
|
||||
if aggregate_props.aggregate_decorator:
|
||||
AggregateDecorator.install(bpy.context)
|
||||
if nest_props.nest_decorator:
|
||||
NestDecorator.install(bpy.context)
|
||||
if model_props.show_wall_axis:
|
||||
WallAxisDecorator.install(bpy.context)
|
||||
if model_props.show_slab_direction:
|
||||
SlabDirectionDecorator.install(bpy.context)
|
||||
if model_props.show_bounding_box:
|
||||
BoundingBoxDecorator.install(bpy.context)
|
||||
|
||||
if preferences.should_use_snap and (scene := bpy.context.scene):
|
||||
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
|
||||
scene.tool_settings.use_snap = True
|
||||
# Match default Bonsai snaps
|
||||
scene.tool_settings.snap_elements_base = {"EDGE", "EDGE_PERPENDICULAR", "VERTEX", "EDGE_MIDPOINT", "FACE"}
|
||||
|
||||
@persistent
|
||||
def load_post(scene):
|
||||
_apply_save_file_invariants(scene)
|
||||
_apply_user_preferences()
|
||||
_install_viewport_overlays()
|
||||
tool.Blender.sync_old_preferences()
|
||||
|
||||
@@ -46,7 +46,7 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
|
||||
class OperationData(TypedDict):
|
||||
id: int
|
||||
guid: NotRequired[str]
|
||||
obj: NotRequired[str]
|
||||
obj: str
|
||||
|
||||
|
||||
class EditObjectOperationData(TypedDict):
|
||||
@@ -64,44 +64,6 @@ class TransactionStep(TypedDict):
|
||||
operations: list[Operation]
|
||||
|
||||
|
||||
# Set when ``IfcStore.get_cache`` observes an external lock on the HDF5 cache —
|
||||
# signal that another Blender process has the same IFC file open. Project panel
|
||||
# polls ``is_cache_locked_by_other_process`` to warn the user. The dismissed
|
||||
# flag is sticky per-session so the warning doesn't re-nag once the user has
|
||||
# acknowledged it.
|
||||
_cache_locked_by_other_process: bool = False
|
||||
_multi_instance_warning_dismissed: bool = False
|
||||
|
||||
|
||||
def is_cache_locked_by_other_process() -> bool:
|
||||
return _cache_locked_by_other_process and not _multi_instance_warning_dismissed
|
||||
|
||||
|
||||
def dismiss_multi_instance_warning() -> None:
|
||||
global _multi_instance_warning_dismissed
|
||||
_multi_instance_warning_dismissed = True
|
||||
|
||||
|
||||
def get_cache_or_detect_lock() -> ifcopenshell.geom.serializers.hdf5 | None:
|
||||
"""Like ``IfcStore.get_cache`` but tracks the multi-instance lock flag — sets
|
||||
it on ``PermissionError``, clears it (along with the dismiss flag) when a
|
||||
subsequent call succeeds. Returns ``None`` on lock; other exceptions
|
||||
propagate. Callers that don't need the warning side effect can use
|
||||
``IfcStore.get_cache`` directly."""
|
||||
global _cache_locked_by_other_process, _multi_instance_warning_dismissed
|
||||
try:
|
||||
cache = IfcStore.get_cache()
|
||||
except PermissionError:
|
||||
_cache_locked_by_other_process = True
|
||||
return None
|
||||
if _cache_locked_by_other_process:
|
||||
# Lock released — clear both flags so a future re-locking re-surfaces
|
||||
# the warning rather than staying suppressed by the previous dismiss.
|
||||
_cache_locked_by_other_process = False
|
||||
_multi_instance_warning_dismissed = False
|
||||
return cache
|
||||
|
||||
|
||||
class IfcStore:
|
||||
path: str = ""
|
||||
"""Should be set only using ``tool.Ifc.set_path``."""
|
||||
@@ -234,7 +196,7 @@ class IfcStore:
|
||||
shutil.copy2(IfcStore.cache_path, new_cache_path)
|
||||
except PermissionError:
|
||||
pass # Well we tried. No cache for you!
|
||||
get_cache_or_detect_lock()
|
||||
IfcStore.get_cache()
|
||||
|
||||
@staticmethod
|
||||
def load_file(path: str) -> None:
|
||||
@@ -354,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:
|
||||
@@ -402,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(
|
||||
@@ -552,7 +519,6 @@ class IfcStore:
|
||||
BrickStore.end_transaction()
|
||||
IfcStore.end_transaction(operator)
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
tool.Parametric.refresh_post_commit(operator)
|
||||
|
||||
if method == "MODAL":
|
||||
cls.modal_in_progress = False
|
||||
@@ -566,19 +532,6 @@ class IfcStore:
|
||||
result = getattr(operator, "_modal")(context, event)
|
||||
except:
|
||||
bonsai.last_error = traceback.format_exc()
|
||||
# An operator that mutated IFC then raised leaves the IFC graph captured
|
||||
# by the transaction but the Blender side stale. Blender does not push an
|
||||
# undo step for a raised operator (mirror of the CANCELLED-modal gap
|
||||
# handled below), so we push one here so Ctrl+Z actually rewinds the
|
||||
# partial mutation, then surface the recovery path to the user.
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file and ifc_file.transaction and ifc_file.transaction.operations:
|
||||
bpy.ops.ed.undo_push(message=f"Recover {operator.bl_idname}")
|
||||
operator.report(
|
||||
{"WARNING"},
|
||||
"Operation partially completed (IFC changed, Blender state may be stale). "
|
||||
"Press Ctrl+Z to restore the previous state.",
|
||||
)
|
||||
# Try to ensure undo will work since Blender undo does work in case of errors.
|
||||
# As error come unexpectedly, it's important that user might have a chance to save the file
|
||||
# before they got the error and not to lose the work they've done.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -223,7 +223,6 @@ class IfcImporter:
|
||||
self.elements: set[ifcopenshell.entity_instance] = set()
|
||||
self.annotations: set[ifcopenshell.entity_instance] = set()
|
||||
self.gross_elements: set[ifcopenshell.entity_instance] = set()
|
||||
self.broken_arrays: set[ifcopenshell.entity_instance] = set()
|
||||
self.element_types: set[ifcopenshell.entity_instance] = set()
|
||||
self.spatial_elements: set[ifcopenshell.entity_instance] = set()
|
||||
self.meshes: dict[str, OBJECT_DATA_TYPE] = {}
|
||||
@@ -980,13 +979,8 @@ class IfcImporter:
|
||||
if unit.Name == "METRE":
|
||||
if not unit.Prefix:
|
||||
bpy.context.scene.unit_settings.length_unit = "METERS"
|
||||
elif f"{unit.Prefix}METERS" in ("KILOMETERS", "CENTIMETERS", "MILLIMETERS", "MICROMETERS"):
|
||||
bpy.context.scene.unit_settings.length_unit = f"{unit.Prefix}METERS"
|
||||
else:
|
||||
# Blender's length_unit enum has no entry for other
|
||||
# SI prefixes (e.g. DECIMETERS), so fall back to
|
||||
# adaptive display instead of failing to open.
|
||||
bpy.context.scene.unit_settings.length_unit = "ADAPTIVE"
|
||||
bpy.context.scene.unit_settings.length_unit = f"{unit.Prefix}METERS"
|
||||
else:
|
||||
bpy.context.scene.unit_settings.system = "IMPERIAL"
|
||||
name = unit.Name.lower()
|
||||
@@ -1103,14 +1097,12 @@ class IfcImporter:
|
||||
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
polyline = None
|
||||
for edge in edges:
|
||||
v1 = vertices[edge[0]]
|
||||
if v1 != v2:
|
||||
polyline = curve.splines.new("POLY")
|
||||
polyline.points[-1].co = mathutils.Vector(v1)
|
||||
v2 = vertices[edge[1]]
|
||||
assert polyline is not None
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = mathutils.Vector(v2)
|
||||
edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist()
|
||||
@@ -1227,18 +1219,8 @@ class IfcImporter:
|
||||
if element not in elements_to_import:
|
||||
continue
|
||||
for i in range(len(data)):
|
||||
tool.Array.set_children_lock_state(element, i, True)
|
||||
tool.Array.constrain_children_to_parent(element)
|
||||
for layer in data:
|
||||
for child_guid in layer.get("children", ()):
|
||||
try:
|
||||
self.file.by_guid(child_guid)
|
||||
except RuntimeError:
|
||||
print(
|
||||
f"setup_arrays: array parent {element.GlobalId} references missing "
|
||||
f"child GUID {child_guid!r}."
|
||||
)
|
||||
self.broken_arrays.add(element)
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
def update_linked_aggregates(self):
|
||||
# TODO Remove this after a while. See commit 17d6b8a
|
||||
|
||||
@@ -20,6 +20,7 @@ import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import ifcopenshell.util.element
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
@@ -27,6 +28,12 @@ from mathutils import Vector
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
def create_bounding_box(objs):
|
||||
# Initialize the bounding box coordinates
|
||||
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
|
||||
@@ -72,8 +79,26 @@ def create_bounding_box(objs):
|
||||
return indices, edges
|
||||
|
||||
|
||||
class AggregateDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_method = "draw_aggregate"
|
||||
class AggregateDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_aggregate, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def dotted_line_shader(self):
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
@@ -128,6 +153,14 @@ class AggregateDecorator(tool.Blender.ViewportDecorator):
|
||||
shader.uniform_float("u_Scale", 25)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_aggregate(self, context):
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
@@ -158,13 +191,12 @@ class AggregateDecorator(tool.Blender.ViewportDecorator):
|
||||
aggregates.append(obj)
|
||||
continue
|
||||
|
||||
aggregate = None
|
||||
aggregates_list = tool.Aggregate.get_aggregates_recursively(element)
|
||||
if props.in_aggregate_mode and props.editing_aggregate:
|
||||
index = aggregates_list.index(tool.Ifc.get_entity(props.editing_aggregate))
|
||||
if index > 0:
|
||||
aggregate = aggregates_list[index - 1]
|
||||
elif aggregates_list:
|
||||
else:
|
||||
aggregate = aggregates_list[-1]
|
||||
if aggregate:
|
||||
aggregates.append(tool.Ifc.get_object(aggregate))
|
||||
@@ -193,11 +225,39 @@ class AggregateDecorator(tool.Blender.ViewportDecorator):
|
||||
self.draw_custom_batch(line, decorator_color_unselected)
|
||||
|
||||
|
||||
class AggregateModeDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_aggregate_name", "POST_PIXEL"),
|
||||
("draw_aggregate_empty", "POST_VIEW"),
|
||||
)
|
||||
class AggregateModeDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_aggregate_name, (context,), "WINDOW", "POST_PIXEL")
|
||||
)
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_aggregate_name(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
|
||||
@@ -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:
|
||||
@@ -139,7 +120,6 @@ class BIMAggregateProperties(PropertyGroup):
|
||||
previous_editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
|
||||
editing_objects: CollectionProperty(type=Objects)
|
||||
not_editing_objects: CollectionProperty(type=Objects)
|
||||
previously_selected_objects: CollectionProperty(type=Objects)
|
||||
aggregate_decorator: BoolProperty(
|
||||
name="Display Aggregate",
|
||||
default=False,
|
||||
@@ -156,6 +136,5 @@ class BIMAggregateProperties(PropertyGroup):
|
||||
previous_editing_aggregate: Union[bpy.types.Object, None]
|
||||
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
previously_selected_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
aggregate_decorator: bool
|
||||
previous_state: bool
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,14 +48,12 @@ def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes)
|
||||
row = layout.row()
|
||||
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
key_prefix = "type." if (element and element.is_a("IfcTypeObject")) else ""
|
||||
for attribute in attributes:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=attribute["name"])
|
||||
value = bonsai.bim.helper.get_display_value(attribute["value"])
|
||||
op = row.operator("bim.select_similar", text=value, icon="NONE", emboss=False)
|
||||
op.key = key_prefix + attribute["name"]
|
||||
op.key = attribute["name"]
|
||||
|
||||
# TODO: reimplement, see #1222
|
||||
# if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -56,6 +56,11 @@ class BoundaryDecorator:
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
@@ -104,11 +109,7 @@ class BoundaryDecorator:
|
||||
|
||||
if unselected_edges:
|
||||
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
|
||||
self.draw_batch(
|
||||
"TRIS", unselected_vertices, tool.Blender.transparent_color(special_elements_color), unselected_tris
|
||||
)
|
||||
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch(
|
||||
"TRIS", selected_vertices, tool.Blender.transparent_color(selected_elements_color), selected_tris
|
||||
)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
|
||||
|
||||
@@ -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"}
|
||||
@@ -843,6 +837,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, opening)
|
||||
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
|
||||
mat.translation = (0, 0, 0)
|
||||
opening_bm = bmesh.new()
|
||||
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
|
||||
for vert in verts:
|
||||
@@ -1059,7 +1054,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return tool.Ifc.get().createIfcConnectionSurfaceGeometry(surface)
|
||||
|
||||
def export_surface(self, polygon, target_face_matrix):
|
||||
ifc_file = tool.Ifc.get()
|
||||
x_axis = target_face_matrix.col[0][:3]
|
||||
z_axis = target_face_matrix.col[2][:3]
|
||||
p1 = target_face_matrix.translation
|
||||
@@ -1072,20 +1066,18 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
placement = builder.create_axis2_placement_3d([o / self.unit_scale for o in p1], z_axis, x_axis)
|
||||
surface.BasisSurface = tool.Ifc.get().create_entity("IfcPlane", placement)
|
||||
|
||||
schema = ifc_file.schema
|
||||
if schema != "IFC2X3":
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in polygon.exterior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
outer_boundary = tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False)
|
||||
|
||||
inner_boundaries: list[ifcopenshell.entity_instance] = []
|
||||
inner_boundaries = []
|
||||
for interior in polygon.interiors:
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in interior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
inner_boundaries.append(tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False))
|
||||
else:
|
||||
# TODO:
|
||||
raise NotImplementedError(schema)
|
||||
pass # TODO
|
||||
|
||||
surface.OuterBoundary = outer_boundary
|
||||
surface.InnerBoundaries = inner_boundaries
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -156,13 +156,16 @@ class BrickschemaReferencesData:
|
||||
for rel in getattr(tool.Ifc.get_entity(bpy.context.active_object), "HasAssociations", []):
|
||||
if rel.is_a("IfcRelAssociatesLibrary"):
|
||||
reference = rel.RelatingLibrary
|
||||
identification = tool.Document.get_external_reference_id(reference)
|
||||
if not identification or "#" not in identification:
|
||||
if tool.Ifc.get_schema() == "IFC2X3" and "#" not in reference.ItemReference:
|
||||
continue
|
||||
if tool.Ifc.get_schema() != "IFC2X3" and "#" not in reference.Identification:
|
||||
continue
|
||||
results.append(
|
||||
{
|
||||
"id": reference.id(),
|
||||
"identification": identification,
|
||||
"identification": (
|
||||
reference.ItemReference if tool.Ifc.get_schema() == "IFC2X3" else reference.Identification
|
||||
),
|
||||
"name": reference.Name or "Unnamed",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
@@ -345,17 +343,9 @@ class CadArcFrom3Points(bpy.types.Operator):
|
||||
class CadOffset(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_offset"
|
||||
bl_label = "CAD Offset"
|
||||
bl_description = (
|
||||
"Offset selected mesh geometry at provided distance, based on the current viewport angle. "
|
||||
"Creates a copy by default, or moves the existing edges if Copy is disabled."
|
||||
)
|
||||
bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
|
||||
copy: bpy.props.BoolProperty(
|
||||
name="Copy",
|
||||
description="Create a new offset copy of the geometry. If disabled, move the existing edges to the offset location",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -400,7 +390,6 @@ class CadOffset(bpy.types.Operator):
|
||||
[verts.update(e.verts) for e in edges]
|
||||
|
||||
# Use the viewport angle to determine the offset direction
|
||||
wp = None
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
# Don't ask me, I don't know.
|
||||
@@ -410,16 +399,10 @@ class CadOffset(bpy.types.Operator):
|
||||
z = area.spaces.active.region_3d.view_rotation @ Vector((0, 0, 1))
|
||||
wp = Matrix([x, y, z, Vector((0, 0, 0))]).to_4x4().transposed()
|
||||
break
|
||||
assert wp is not None
|
||||
|
||||
rotation = Matrix.Rotation(pi / 2, 2, "Z")
|
||||
rotation_i = Matrix.Rotation(-pi / 2, 2, "Z")
|
||||
|
||||
# When not copying, the offset positions are gathered here and applied to
|
||||
# the existing verts only after all loops are processed, so that the
|
||||
# original coordinates are still available while computing offsets.
|
||||
moved_verts = []
|
||||
|
||||
# Create loops from edges
|
||||
loop_edges = set(edges)
|
||||
loops = []
|
||||
@@ -532,15 +515,12 @@ class CadOffset(bpy.types.Operator):
|
||||
offset_length = self.distance / sqrt((1 + normals[0].dot(normals[1])) / 2)
|
||||
offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ (new_normal * offset_length).to_3d())
|
||||
new_vert = v1.co + offset
|
||||
new_verts.append(bm.verts.new(new_vert))
|
||||
else:
|
||||
normal = (normals[0] * self.distance).to_3d()
|
||||
offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ normal)
|
||||
new_vert = v1.co + offset
|
||||
|
||||
if self.copy:
|
||||
new_verts.append(bm.verts.new(new_vert))
|
||||
else:
|
||||
moved_verts.append((v1, new_vert))
|
||||
|
||||
processed_verts.add(v1.index)
|
||||
|
||||
@@ -549,14 +529,9 @@ class CadOffset(bpy.types.Operator):
|
||||
|
||||
v1 = v2
|
||||
|
||||
if self.copy:
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
if is_closed:
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
# Move the existing edges to the offset location.
|
||||
for vert, new_co in moved_verts:
|
||||
vert.co = new_co
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
if is_closed:
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
@@ -27,11 +27,6 @@ class BIMCadProperties(PropertyGroup):
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
|
||||
copy: bpy.props.BoolProperty(
|
||||
name="Copy",
|
||||
description="Create a new offset copy of the geometry. If disabled, move the existing edges to the offset location",
|
||||
default=True,
|
||||
)
|
||||
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
|
||||
gable_roof_edge_angle: bpy.props.FloatProperty(
|
||||
@@ -42,7 +37,6 @@ class BIMCadProperties(PropertyGroup):
|
||||
resolution: int
|
||||
radius: float
|
||||
distance: float
|
||||
copy: bool
|
||||
x: float
|
||||
y: float
|
||||
gable_roof_edge_angle: float
|
||||
|
||||
@@ -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):
|
||||
@@ -256,8 +218,6 @@ class CadHotkey(bpy.types.Operator):
|
||||
elif self.hotkey == "S_O":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "distance")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "copy")
|
||||
|
||||
elif self.hotkey == "S_R":
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
@@ -293,7 +253,7 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
|
||||
|
||||
def hotkey_S_O(self):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance, copy=self.props.copy)
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
obj = bpy.context.active_object
|
||||
|
||||
@@ -18,17 +18,43 @@
|
||||
|
||||
import blf
|
||||
import gpu
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class ClashDecorator(tool.Blender.ViewportDecorator):
|
||||
draw_methods = (
|
||||
("draw_text", "POST_PIXEL"),
|
||||
("draw_geometry", "POST_VIEW"),
|
||||
)
|
||||
class ClashDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_text(self, context):
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -478,7 +478,6 @@ class ChangeClassificationLevel(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Classification.get_classification_props()
|
||||
props.available_library_references.clear()
|
||||
reference = None
|
||||
for reference in IfcStore.classification_file.by_id(self.parent_id).HasReferences:
|
||||
new = props.available_library_references.add()
|
||||
new.identification = reference.Identification or ""
|
||||
@@ -486,7 +485,6 @@ class ChangeClassificationLevel(bpy.types.Operator):
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.has_references = bool(reference.HasReferences)
|
||||
new.referenced_source
|
||||
assert reference
|
||||
if reference.ReferencedSource.is_a("IfcClassificationReference"):
|
||||
props.active_library_referenced_source = reference.ReferencedSource.ReferencedSource.id()
|
||||
else:
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 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/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
from . import face_quad, gizmos, operator, prop, ui
|
||||
|
||||
classes = (
|
||||
operator.BIM_OT_add_clip_box,
|
||||
operator.BIM_OT_add_clip_box_for_source,
|
||||
operator.BIM_OT_align_view_to_clip_face,
|
||||
operator.BIM_OT_duplicate_clip_box,
|
||||
operator.BIM_OT_remove_clip_box,
|
||||
operator.BIM_OT_set_active_clip_box,
|
||||
operator.BIM_OT_toggle_clip_box_enabled,
|
||||
prop.BIMClipBoxProperties,
|
||||
prop.BIMSceneClipBoxProperties,
|
||||
face_quad.BIM_GT_box_face_quad,
|
||||
face_quad.BIM_GT_box_face_outline,
|
||||
gizmos.OBJECT_GGT_bim_clip_box,
|
||||
ui.BIM_MT_clip_box_add_for_source,
|
||||
ui.BIM_MT_clip_box_info,
|
||||
ui.BIM_MT_clip_box_settings,
|
||||
ui.BIM_UL_clip_box,
|
||||
ui.BIM_PT_clip_box,
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def _on_depsgraph_update(scene, depsgraph):
|
||||
tool.ClipBox.on_depsgraph_update(scene, depsgraph)
|
||||
tool.ClipBox.on_depsgraph_update_caps(scene, depsgraph)
|
||||
|
||||
|
||||
@persistent
|
||||
def _on_load_pre(filepath):
|
||||
# Tear down any in-flight clip-box timers before Blender frees the
|
||||
# WM / screens / areas / regions for the loading file. A refresh timer
|
||||
# that survives the teardown fires against the new file's freshly-
|
||||
# allocated regions before their GPU state is wired, CTD-ing inside
|
||||
# GPU_matrix_ortho_set. The gate also blocks the depsgraph IFC-reload
|
||||
# branch and is held closed until on_pre_view fires for the first time
|
||||
# on the new file (first paint = GPU contexts wired).
|
||||
tool.ClipBox._file_loading = True
|
||||
tool.ClipBox._post_load_paint_pending = True
|
||||
tool.ClipBox._cancel_pending_refresh()
|
||||
tool.ClipBox._cancel_pending_cap_rebuild()
|
||||
|
||||
|
||||
@persistent
|
||||
def _on_load_post(filepath):
|
||||
# The _file_loading gate is NOT cleared here: load_post fires before
|
||||
# the new file's first paint, so GPU contexts may still be uninitialised.
|
||||
# on_pre_view consumes _post_load_paint_pending to open the gate at the
|
||||
# safe moment and kick the post-load re-arm.
|
||||
# Restore the per-scene clip-box list from the project's BBIM_ClipBoxes
|
||||
# pset. Runs after the standard load_post that creates Blender objects.
|
||||
tool.ClipBox._last_seen_object_matrices.clear()
|
||||
tool.ClipBox.load_from_project_pset()
|
||||
|
||||
|
||||
_draw_handler_pre = None
|
||||
_draw_handler_post = None
|
||||
|
||||
|
||||
def register():
|
||||
global _draw_handler_pre, _draw_handler_post
|
||||
bpy.types.Object.BIMClipBoxProperties = bpy.props.PointerProperty(type=prop.BIMClipBoxProperties)
|
||||
bpy.types.Scene.BIMSceneClipBoxProperties = bpy.props.PointerProperty(type=prop.BIMSceneClipBoxProperties)
|
||||
tool.ClipBox.reset_ownership()
|
||||
if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update)
|
||||
if _on_load_pre not in bpy.app.handlers.load_pre:
|
||||
bpy.app.handlers.load_pre.append(_on_load_pre)
|
||||
if _on_load_post not in bpy.app.handlers.load_post:
|
||||
bpy.app.handlers.load_post.append(_on_load_post)
|
||||
if _draw_handler_pre is None:
|
||||
_draw_handler_pre = bpy.types.SpaceView3D.draw_handler_add(tool.ClipBox.on_pre_view, (), "WINDOW", "PRE_VIEW")
|
||||
if _draw_handler_post is None:
|
||||
_draw_handler_post = bpy.types.SpaceView3D.draw_handler_add(
|
||||
tool.ClipBox.on_post_view_caps, (), "WINDOW", "POST_VIEW"
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
global _draw_handler_pre, _draw_handler_post
|
||||
if _draw_handler_post is not None:
|
||||
try:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(_draw_handler_post, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
_draw_handler_post = None
|
||||
if _draw_handler_pre is not None:
|
||||
try:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(_draw_handler_pre, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
_draw_handler_pre = None
|
||||
if _on_load_post in bpy.app.handlers.load_post:
|
||||
bpy.app.handlers.load_post.remove(_on_load_post)
|
||||
if _on_load_pre in bpy.app.handlers.load_pre:
|
||||
bpy.app.handlers.load_pre.remove(_on_load_pre)
|
||||
if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update)
|
||||
tool.ClipBox._cancel_pending_refresh()
|
||||
tool.ClipBox._cancel_pending_cap_rebuild()
|
||||
tool.ClipBox._last_seen_object_matrices.clear()
|
||||
tool.ClipBox.clear_clip_planes()
|
||||
del bpy.types.Object.BIMClipBoxProperties
|
||||
del bpy.types.Scene.BIMSceneClipBoxProperties
|
||||
@@ -1,212 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 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/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""EnumProperty ``items=`` callbacks for the source-based clip-box picker.
|
||||
|
||||
Each callback returns ``[(id_str, label, description)]`` where ``id_str`` is
|
||||
an IFC entity id stringified for entity-driven kinds, an IFC class name for
|
||||
``CLASS``, or a fixed status name for ``STATUS``. The clip-box operator
|
||||
turns the picked id into a ``matrix_world`` via the source-preset helper.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
EnumItems = list[tuple[str, str, str]]
|
||||
|
||||
# Module-level cache. Blender's EnumProperty stores raw char pointers from the
|
||||
# tuples a callback returns, so the Python strings must outlive the draw call.
|
||||
# Stashing the latest result per kind keeps them alive across callback firings.
|
||||
_items_cache: dict[str, EnumItems] = {}
|
||||
|
||||
# Sentinel id used for the "no options available" placeholder. The operator
|
||||
# treats this as an invalid pick and surfaces an ERROR.
|
||||
NO_OPTIONS_ID = "__none__"
|
||||
|
||||
|
||||
def _cache(kind: str, items: EnumItems) -> EnumItems:
|
||||
_items_cache[kind] = items
|
||||
return items
|
||||
|
||||
|
||||
def _no_options(label: str) -> EnumItems:
|
||||
# Blender refuses to draw an EnumProperty with zero entries — show a
|
||||
# placeholder so the dialog renders and the user sees the empty state.
|
||||
return [(NO_OPTIONS_ID, label, "")]
|
||||
|
||||
|
||||
def _label(entity, ifc_class: str | None = None) -> str:
|
||||
name = (getattr(entity, "Name", None) or "Unnamed").strip() or "Unnamed"
|
||||
return f"{ifc_class}: {name}" if ifc_class else name
|
||||
|
||||
|
||||
def _build_items(kind: str, empty_label: str, build_fn) -> EnumItems:
|
||||
"""Shared shape for the IFC-driven enum callbacks.
|
||||
|
||||
Returns the no-IFC placeholder if no file is loaded, then runs
|
||||
``build_fn(ifc_file)``, sorts the result alphabetically by label, and
|
||||
returns the empty-result placeholder if nothing matched. The output is
|
||||
always routed through the module cache.
|
||||
"""
|
||||
ifc = tool.Ifc.get()
|
||||
if ifc is None:
|
||||
return _cache(kind, _no_options("No IFC loaded"))
|
||||
items = build_fn(ifc)
|
||||
items.sort(key=lambda t: t[1].lower())
|
||||
if not items:
|
||||
return _cache(kind, _no_options(empty_label))
|
||||
return _cache(kind, items)
|
||||
|
||||
|
||||
# Top-down spatial hierarchy so the picker reads in the order an architect
|
||||
# already thinks in, rather than a flat alphabetical mix. IfcSpace is excluded
|
||||
# — spaces are typically empty volumes used for room metadata, so clipping to
|
||||
# one rarely matches the user intent of "show me what's in this container".
|
||||
SPATIAL_CLASSES: tuple[str, ...] = (
|
||||
"IfcProject",
|
||||
"IfcSite",
|
||||
"IfcBuilding",
|
||||
"IfcBuildingStorey",
|
||||
)
|
||||
|
||||
|
||||
def spatial_items(self, context) -> EnumItems:
|
||||
# Special-case: per-class sort within the hierarchy order rather than a
|
||||
# flat alphabetical sort, so the dropdown reads project → site → building.
|
||||
ifc = tool.Ifc.get()
|
||||
if ifc is None:
|
||||
return _cache("SPATIAL", _no_options("No IFC loaded"))
|
||||
items: EnumItems = []
|
||||
for ifc_class in SPATIAL_CLASSES:
|
||||
try:
|
||||
entities = ifc.by_type(ifc_class, include_subtypes=False)
|
||||
except RuntimeError:
|
||||
continue
|
||||
for entity in sorted(entities, key=lambda e: (e.Name or "").lower()):
|
||||
items.append((str(entity.id()), _label(entity, ifc_class), ""))
|
||||
if not items:
|
||||
return _cache("SPATIAL", _no_options("No spatial containers"))
|
||||
return _cache("SPATIAL", items)
|
||||
|
||||
|
||||
def class_items(self, context) -> EnumItems:
|
||||
# Special-case: the picker value IS the IFC class name, not an entity id,
|
||||
# so the build shape differs from the other entity-driven callbacks.
|
||||
ifc = tool.Ifc.get()
|
||||
if ifc is None:
|
||||
return _cache("CLASS", _no_options("No IFC loaded"))
|
||||
# List only IFC classes ACTUALLY present in the file (not the whole
|
||||
# schema), so the user picks from classes that can produce a non-empty
|
||||
# clip volume. ``e.is_a()`` returns the most specific class per element.
|
||||
present = sorted({e.is_a() for e in ifc.by_type("IfcProduct")})
|
||||
if not present:
|
||||
return _cache("CLASS", _no_options("No products"))
|
||||
return _cache("CLASS", [(cls, cls, "") for cls in present])
|
||||
|
||||
|
||||
def type_items(self, context) -> EnumItems:
|
||||
return _build_items(
|
||||
"TYPE",
|
||||
"No types defined",
|
||||
lambda ifc: [(str(e.id()), _label(e, e.is_a()), "") for e in ifc.by_type("IfcTypeProduct")],
|
||||
)
|
||||
|
||||
|
||||
def material_items(self, context) -> EnumItems:
|
||||
return _build_items(
|
||||
"MATERIAL",
|
||||
"No materials defined",
|
||||
lambda ifc: [(str(e.id()), _label(e), "") for e in ifc.by_type("IfcMaterial")],
|
||||
)
|
||||
|
||||
|
||||
def profile_items(self, context) -> EnumItems:
|
||||
# ProfileName is optional. Skip unnamed profiles — they can't be
|
||||
# meaningfully picked from a flat list.
|
||||
return _build_items(
|
||||
"PROFILE",
|
||||
"No named profiles",
|
||||
lambda ifc: [
|
||||
(str(e.id()), f"{e.is_a()}: {e.ProfileName}", "")
|
||||
for e in ifc.by_type("IfcProfileDef")
|
||||
if getattr(e, "ProfileName", None)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def drawing_items(self, context) -> EnumItems:
|
||||
return _build_items(
|
||||
"DRAWING",
|
||||
"No drawings defined",
|
||||
lambda ifc: [(str(e.id()), _label(e), "") for e in ifc.by_type("IfcAnnotation") if e.ObjectType == "DRAWING"],
|
||||
)
|
||||
|
||||
|
||||
# Display labels for each status value. The id strings on the left are the
|
||||
# canonical Pset_*Common.Status enum values accepted by Bonsai's status query.
|
||||
STATUS_LABELS: tuple[tuple[str, str], ...] = (
|
||||
("No Status", "No Status"),
|
||||
("NEW", "New"),
|
||||
("EXISTING", "Existing"),
|
||||
("DEMOLISH", "Demolish"),
|
||||
("TEMPORARY", "Temporary"),
|
||||
("OTHER", "Other"),
|
||||
("NOTKNOWN", "Not Known"),
|
||||
("UNSET", "Unset"),
|
||||
)
|
||||
|
||||
|
||||
def status_items(self, context) -> EnumItems:
|
||||
# Fixed enum; no IFC needed. Still routed through the cache to share the
|
||||
# same string-lifetime guarantee as the other callbacks.
|
||||
return _cache("STATUS", [(value, label, "") for value, label in STATUS_LABELS])
|
||||
|
||||
|
||||
def system_items(self, context) -> EnumItems:
|
||||
# IfcStructuralAnalysisModel is a structural-grouping container, not a
|
||||
# distribution system — excluded to match Bonsai's other system pickers.
|
||||
return _build_items(
|
||||
"SYSTEM",
|
||||
"No systems defined",
|
||||
lambda ifc: [
|
||||
(str(e.id()), _label(e, e.is_a()), "")
|
||||
for e in ifc.by_type("IfcSystem")
|
||||
if not e.is_a("IfcStructuralAnalysisModel")
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def group_items(self, context) -> EnumItems:
|
||||
# include_subtypes=False so IfcSystem and IfcZone instances don't appear
|
||||
# under Group as well — those get their own picker entries.
|
||||
return _build_items(
|
||||
"GROUP",
|
||||
"No groups defined",
|
||||
lambda ifc: [(str(e.id()), _label(e), "") for e in ifc.by_type("IfcGroup", include_subtypes=False)],
|
||||
)
|
||||
|
||||
|
||||
def zone_items(self, context) -> EnumItems:
|
||||
return _build_items(
|
||||
"ZONE",
|
||||
"No zones defined",
|
||||
lambda ifc: [(str(e.id()), _label(e), "") for e in ifc.by_type("IfcZone")],
|
||||
)
|
||||
@@ -1,879 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 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/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Generic face-quad resize gizmos for any axis-aligned local box.
|
||||
|
||||
This module contains the box-agnostic core of the interactive
|
||||
face-resize gizmos: two Gizmo classes (a near-invisible click target
|
||||
welded to each face, and a thin colored edge outline), a per-redraw
|
||||
orchestrator that places six of each on a box, and the pure one-sided
|
||||
resize arithmetic. None of it knows about IFC, clip boxes, or
|
||||
``BIMSceneClipBoxProperties`` — a future camera-view-box adapter can
|
||||
reuse the same classes and helpers.
|
||||
|
||||
Consumer contract — the adapter group must:
|
||||
|
||||
1. Create six ``BIM_GT_box_face_quad`` and six ``BIM_GT_box_face_outline``
|
||||
instances at ``setup()`` time, in :data:`FACE_ROUTES` order, and bind
|
||||
each quad's ``move_get_cb`` / ``move_set_cb`` to closures that read
|
||||
and mutate the box's host (e.g. an Empty's ``location`` / ``scale``).
|
||||
2. Call :func:`apply_face_quad_layout` from ``refresh()`` /
|
||||
``draw_prepare()`` with the box's local-frame ``bmin`` / ``bmax``,
|
||||
the host's ``matrix_world``, the OBB rotation as a 4x4
|
||||
(``Matrix.Identity(4)`` when the rotation rides in ``matrix_world``),
|
||||
and the current ``region`` / ``rv3d``.
|
||||
3. Implement ``_lock_for(active_gz)`` / ``_unlock_all()`` on the group
|
||||
for drag mutual exclusion; the quad's ``invoke`` / ``exit`` call them.
|
||||
|
||||
The resize arithmetic in :func:`compute_face_resize` is pure: feed it
|
||||
the modal scalar plus drag-start snapshots and it returns the host's
|
||||
new scale-on-axis and new origin location.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d, region_2d_to_location_3d
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public iteration order
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# (axis, is_max) pairs. The adapter group's ``setup()`` MUST create its
|
||||
# six face-quad gizmos in this order so positional indexing into the
|
||||
# layout helper stays correct.
|
||||
FACE_ROUTES: tuple[tuple[int, bool], ...] = (
|
||||
(0, False),
|
||||
(0, True),
|
||||
(1, False),
|
||||
(1, True),
|
||||
(2, False),
|
||||
(2, True),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public visual constants (adapter reads these in setup())
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Standard XYZ axis colors (Blender convention).
|
||||
AXIS_COLOR: dict[int, tuple[float, float, float]] = {
|
||||
0: (1.0, 0.2, 0.2),
|
||||
1: (0.2, 1.0, 0.2),
|
||||
2: (0.2, 0.4, 1.0),
|
||||
}
|
||||
|
||||
# Documented "selectable but unpainted" trick: the GPU still writes the
|
||||
# selection buffer at this alpha so clicks register, but no visible
|
||||
# pixels are produced.
|
||||
FACE_QUAD_ALPHA: float = 0.001
|
||||
|
||||
# Very faint hover tint — just enough to confirm "you're aiming at this
|
||||
# face" without painting visibly over geometry behind it.
|
||||
FACE_QUAD_ALPHA_HIGHLIGHT: float = 0.04
|
||||
|
||||
# Setup-time default for ``select_bias``; the layout helper overwrites
|
||||
# it per frame to the front-facing or halo value below. Kept below the
|
||||
# canonical arrow bias so a bailed frame can't let a front quad steal
|
||||
# clicks meant for a hidden control.
|
||||
FACE_QUAD_SELECT_BIAS: float = 0.5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Unit quad in the local XY plane spanning [-0.5, 0.5]^2 at z=0. Two
|
||||
# CCW triangles viewed from +Z. matrix_basis stretches it onto the
|
||||
# face's perpendicular extents.
|
||||
_QUAD_TRIS: list[tuple[float, float, float]] = [
|
||||
(-0.5, -0.5, 0.0),
|
||||
(0.5, -0.5, 0.0),
|
||||
(0.5, 0.5, 0.0),
|
||||
(-0.5, -0.5, 0.0),
|
||||
(0.5, 0.5, 0.0),
|
||||
(-0.5, 0.5, 0.0),
|
||||
]
|
||||
|
||||
# Unit-quad outline as 4 line segments in the local XY plane at z=0.
|
||||
_QUAD_OUTLINE_LINES: list[tuple[float, float, float]] = [
|
||||
(-0.5, -0.5, 0.0),
|
||||
(0.5, -0.5, 0.0),
|
||||
(0.5, -0.5, 0.0),
|
||||
(0.5, 0.5, 0.0),
|
||||
(0.5, 0.5, 0.0),
|
||||
(-0.5, 0.5, 0.0),
|
||||
(-0.5, 0.5, 0.0),
|
||||
(-0.5, -0.5, 0.0),
|
||||
]
|
||||
|
||||
# Degenerate zero-area triangle for hidden back-facing quads with no
|
||||
# visible-adjacent neighbours (rare orientation). Blender tolerates
|
||||
# this; the gizmo is hidden anyway so nothing renders.
|
||||
_EMPTY_TRIS: list[tuple[float, float, float]] = [
|
||||
(0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0),
|
||||
]
|
||||
|
||||
# Rotates the gizmo's local +Z onto the outward face normal in the
|
||||
# box's local frame. Right-hand rotation around the named axis.
|
||||
_AXIS_ORIENT: dict[tuple[int, bool], Matrix] = {
|
||||
(0, False): Matrix.Rotation(-math.pi / 2, 4, "Y"),
|
||||
(0, True): Matrix.Rotation(math.pi / 2, 4, "Y"),
|
||||
(1, False): Matrix.Rotation(math.pi / 2, 4, "X"),
|
||||
(1, True): Matrix.Rotation(-math.pi / 2, 4, "X"),
|
||||
(2, False): Matrix.Rotation(math.pi, 4, "X"),
|
||||
(2, True): Matrix.Identity(4),
|
||||
}
|
||||
|
||||
# Per-face mapping from face-quad local axes to local box axes for the
|
||||
# perpendicular-extent scale. ``(w_axis, h_axis)`` — the box-local axis
|
||||
# indices the quad's local X and Y span after the orientation rotation.
|
||||
_QUAD_PERP_AXES: dict[tuple[int, bool], tuple[int, int]] = {
|
||||
(0, False): (2, 1),
|
||||
(0, True): (2, 1),
|
||||
(1, False): (0, 2),
|
||||
(1, True): (0, 2),
|
||||
(2, False): (0, 1),
|
||||
(2, True): (0, 1),
|
||||
}
|
||||
|
||||
# Front-facing quad sits ABOVE the halo strips so the cursor on the
|
||||
# visible face area always grabs the visible face, never accidentally
|
||||
# routes to a back-face halo strip in an adjacent screen region.
|
||||
_FACE_QUAD_FRONT_FACING_SELECT_BIAS: float = 1.5
|
||||
_FACE_QUAD_HALO_FRAME_SELECT_BIAS: float = 1.0
|
||||
|
||||
# Target halo-strip thickness in screen pixels. The world-space margin
|
||||
# is recomputed per frame so the rim stays a roughly constant on-screen
|
||||
# size regardless of viewport zoom.
|
||||
_FACE_QUAD_HALO_TARGET_PIXELS: float = 20.0
|
||||
|
||||
# Minimum world half-extent a face resize may shrink to. Stops a drag
|
||||
# from collapsing the host to zero or negative scale.
|
||||
_MIN_HALF_EXTENT: float = 1e-4
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure predicates (testable without Blender)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Vec3 = tuple[float, float, float]
|
||||
|
||||
|
||||
def face_outward_axis_local(axis: int, is_max: bool) -> Vec3:
|
||||
"""Un-rotated outward face normal in the box's local AABB coords.
|
||||
|
||||
For ``(axis=0, is_max=True)`` returns ``(+1, 0, 0)``; for the −X
|
||||
face ``(-1, 0, 0)``; etc. The rotated world normal is obtained by
|
||||
applying the host's rotation and the OBB rotation:
|
||||
``mw_rot @ cage_rotation @ this``.
|
||||
"""
|
||||
sign = 1.0 if is_max else -1.0
|
||||
out = [0.0, 0.0, 0.0]
|
||||
out[axis] = sign
|
||||
return (out[0], out[1], out[2])
|
||||
|
||||
|
||||
def front_facing_face_mask(
|
||||
face_normals_world: Sequence[Vec3],
|
||||
view_dir_world: Vec3,
|
||||
eps: float = 1e-6,
|
||||
) -> tuple[bool, ...]:
|
||||
"""Which of the 6 box faces point toward the camera.
|
||||
|
||||
A face is front-facing iff its outward normal points AGAINST the
|
||||
view direction (``dot(normal, view_dir) < -eps``). The ``-eps``
|
||||
margin prevents flicker at grazing angles.
|
||||
|
||||
``face_normals_world`` must be in :data:`FACE_ROUTES` order; returns
|
||||
a 6-tuple of bool parallel to that order.
|
||||
"""
|
||||
if len(face_normals_world) != 6:
|
||||
msg = f"expected 6 face normals, got {len(face_normals_world)}"
|
||||
raise ValueError(msg)
|
||||
vx, vy, vz = view_dir_world
|
||||
return tuple((n[0] * vx + n[1] * vy + n[2] * vz) < -eps for n in face_normals_world)
|
||||
|
||||
|
||||
def view_axis_parallel_face_mask(
|
||||
face_normals_world: Sequence[Vec3],
|
||||
view_dir_world: Vec3,
|
||||
threshold: float = 0.95,
|
||||
) -> tuple[bool, ...]:
|
||||
"""Which faces have normals (anti-)parallel to the view direction.
|
||||
|
||||
True iff ``abs(dot(normal, view_dir)) >= threshold`` — i.e. the
|
||||
face is nearly perpendicular to the screen plane. Provided as a
|
||||
pure predicate for callers that want to detect degenerate-drag
|
||||
conditions; the layout helper itself no longer gates on it.
|
||||
"""
|
||||
if len(face_normals_world) != 6:
|
||||
msg = f"expected 6 face normals, got {len(face_normals_world)}"
|
||||
raise ValueError(msg)
|
||||
vx, vy, vz = view_dir_world
|
||||
return tuple(abs(n[0] * vx + n[1] * vy + n[2] * vz) >= threshold for n in face_normals_world)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure resize arithmetic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def compute_face_resize(
|
||||
*,
|
||||
value: float,
|
||||
init_world_half: float,
|
||||
init_location: tuple[float, float, float],
|
||||
world_axis: tuple[float, float, float],
|
||||
display_size: float,
|
||||
) -> tuple[float, tuple[float, float, float]]:
|
||||
"""Pure one-sided face-resize arithmetic.
|
||||
|
||||
Returns ``(new_scale_axis, new_location)`` — the host's new scale
|
||||
on the dragged axis and its new world origin — such that the
|
||||
dragged face moves by the modal's outward delta while the OPPOSITE
|
||||
face stays put.
|
||||
|
||||
``value`` is ``init + delta``, where ``init`` is the unsigned
|
||||
drag-start world half-extent and ``delta`` is the cursor projection
|
||||
onto the face's OUTWARD world normal. Realized half-extent is
|
||||
clamped to a small floor; the location shift uses the realized
|
||||
(post-clamp) delta so the opposite face stays fixed even at the
|
||||
clamp.
|
||||
"""
|
||||
face_delta = value - init_world_half
|
||||
new_world_half = init_world_half + 0.5 * face_delta
|
||||
if new_world_half < _MIN_HALF_EXTENT:
|
||||
new_world_half = _MIN_HALF_EXTENT
|
||||
realized_delta = 2.0 * (new_world_half - init_world_half)
|
||||
|
||||
ds = display_size if display_size != 0.0 else 1.0
|
||||
new_scale_axis = new_world_half / ds
|
||||
shift = 0.5 * realized_delta
|
||||
new_location = (
|
||||
init_location[0] + shift * world_axis[0],
|
||||
init_location[1] + shift * world_axis[1],
|
||||
init_location[2] + shift * world_axis[2],
|
||||
)
|
||||
return new_scale_axis, new_location
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal geometry helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _compute_face_quad_scale(bmin: Any, bmax: Any, axis: int, is_max: bool) -> tuple[float, float]:
|
||||
"""Return ``(w, h)`` for the face quad's scale matrix."""
|
||||
w_axis, h_axis = _QUAD_PERP_AXES[(axis, is_max)]
|
||||
w = float(bmax[w_axis] - bmin[w_axis])
|
||||
h = float(bmax[h_axis] - bmin[h_axis])
|
||||
return w, h
|
||||
|
||||
|
||||
def _shared_edge_corner_keys(
|
||||
axis_a: int, is_max_a: bool, axis_b: int, is_max_b: bool
|
||||
) -> tuple[tuple[int, int, int], tuple[int, int, int]] | None:
|
||||
"""Return the 2 corner-bit triples shared by two adjacent faces.
|
||||
|
||||
Corner keys are 3-tuples of bits (0 = bmin, 1 = bmax). The two
|
||||
returned corners are ordered with the free-axis bit ascending.
|
||||
"""
|
||||
if axis_a == axis_b:
|
||||
return None
|
||||
free_axis = 3 - axis_a - axis_b
|
||||
bit_a = 1 if is_max_a else 0
|
||||
bit_b = 1 if is_max_b else 0
|
||||
corner_lo = [0, 0, 0]
|
||||
corner_hi = [0, 0, 0]
|
||||
corner_lo[axis_a] = bit_a
|
||||
corner_hi[axis_a] = bit_a
|
||||
corner_lo[axis_b] = bit_b
|
||||
corner_hi[axis_b] = bit_b
|
||||
corner_lo[free_axis] = 0
|
||||
corner_hi[free_axis] = 1
|
||||
return (
|
||||
(corner_lo[0], corner_lo[1], corner_lo[2]),
|
||||
(corner_hi[0], corner_hi[1], corner_hi[2]),
|
||||
)
|
||||
|
||||
|
||||
def _face_corner_keys(axis: int, is_max: bool) -> tuple[
|
||||
tuple[int, int, int],
|
||||
tuple[int, int, int],
|
||||
tuple[int, int, int],
|
||||
tuple[int, int, int],
|
||||
]:
|
||||
"""Return the 4 corner-bit triples of a face in CCW order.
|
||||
|
||||
Triangulation as ``[(0,1,2), (0,2,3)]`` covers the whole face with
|
||||
two non-overlapping triangles.
|
||||
"""
|
||||
fixed_bit = 1 if is_max else 0
|
||||
free_axes = [a for a in (0, 1, 2) if a != axis]
|
||||
fa0, fa1 = free_axes
|
||||
corners = []
|
||||
for ka, kb in ((0, 0), (1, 0), (1, 1), (0, 1)):
|
||||
key = [0, 0, 0]
|
||||
key[axis] = fixed_bit
|
||||
key[fa0] = ka
|
||||
key[fa1] = kb
|
||||
corners.append((key[0], key[1], key[2]))
|
||||
return (corners[0], corners[1], corners[2], corners[3])
|
||||
|
||||
|
||||
def _build_strip_tris_relative(
|
||||
edge_p0_local: tuple[float, float, float],
|
||||
edge_p1_local: tuple[float, float, float],
|
||||
extrusion_local: tuple[float, float, float],
|
||||
) -> list[tuple[float, float, float]]:
|
||||
"""Build two CCW triangles (6 vertices) for a thin halo strip.
|
||||
|
||||
All inputs are in coords relative to the gizmo's ``matrix_basis``
|
||||
anchor. The strip runs along ``[edge_p0_local, edge_p1_local]`` and
|
||||
extrudes by ``extrusion_local`` perpendicular to the edge.
|
||||
"""
|
||||
p0x, p0y, p0z = edge_p0_local
|
||||
p1x, p1y, p1z = edge_p1_local
|
||||
ex, ey, ez = extrusion_local
|
||||
p0e = (p0x + ex, p0y + ey, p0z + ez)
|
||||
p1e = (p1x + ex, p1y + ey, p1z + ez)
|
||||
return [
|
||||
(p0x, p0y, p0z),
|
||||
p0e,
|
||||
p1e,
|
||||
(p0x, p0y, p0z),
|
||||
p1e,
|
||||
(p1x, p1y, p1z),
|
||||
]
|
||||
|
||||
|
||||
def _strips_geometry_changed(quad_gz, face_quad_local, all_tris) -> bool:
|
||||
"""True if the back-face quad's geometry differs from the cached upload.
|
||||
|
||||
Pure orbit/pan doesn't change either the box pose or the cage
|
||||
rotation, so the computed strip vertices are byte-identical to the
|
||||
previous frame's. Hitting the cache lets the back-facing branch
|
||||
skip ``new_custom_shape`` and the GPU upload.
|
||||
"""
|
||||
cached = getattr(quad_gz, "_strips_cache_key", None)
|
||||
last_state = getattr(quad_gz, "_last_geometry_state", None)
|
||||
key = (face_quad_local, all_tris)
|
||||
if cached is None or last_state != "strips" or cached != key:
|
||||
quad_gz._strips_cache_key = key
|
||||
quad_gz._last_geometry_state = "strips"
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _compute_face_basis(
|
||||
mw: Any,
|
||||
mw_rot: Any,
|
||||
cage_rotation: Any,
|
||||
pivot_local: Any,
|
||||
face_local: Any,
|
||||
orient: Any,
|
||||
) -> tuple[Any, Any]:
|
||||
"""World-space (translation, outward-normal-direction) for one face."""
|
||||
rotated_face_local = cage_rotation.to_3x3() @ (face_local - pivot_local) + pivot_local
|
||||
face_world = mw @ rotated_face_local
|
||||
world_axis = (mw_rot @ cage_rotation.to_3x3() @ (orient.to_3x3() @ Vector((0.0, 0.0, 1.0)))).normalized()
|
||||
return face_world, world_axis
|
||||
|
||||
|
||||
def _compose_face_matrix_basis(
|
||||
face_world: Any,
|
||||
mw_rot_scale: Any,
|
||||
cage_rotation: Any,
|
||||
orient: Any,
|
||||
w: float,
|
||||
h: float,
|
||||
) -> Any:
|
||||
"""Compose the 5-term ``matrix_basis`` for a face-plane gizmo.
|
||||
|
||||
Returns ``Translation @ mw_rot_scale @ cage_rotation @ orient @
|
||||
Diagonal((w, h, 1, 1))`` — maps a unit-square local quad onto the
|
||||
world-space face rectangle, including the host's scale.
|
||||
"""
|
||||
quad_scale = Matrix.Diagonal((w, h, 1.0, 1.0))
|
||||
return Matrix.Translation(face_world) @ mw_rot_scale.to_4x4() @ cage_rotation @ orient @ quad_scale
|
||||
|
||||
|
||||
def _compute_box_corners_world(
|
||||
bmin: Any,
|
||||
bmax: Any,
|
||||
pivot_local: Any,
|
||||
cage_rotation_3x3: Any,
|
||||
mw: Any,
|
||||
) -> dict[tuple[int, int, int], Any]:
|
||||
"""Return the 8 OBB corners in world space, keyed by bit-triple."""
|
||||
corners: dict[tuple[int, int, int], Any] = {}
|
||||
for ix in (0, 1):
|
||||
for iy in (0, 1):
|
||||
for iz in (0, 1):
|
||||
local = Vector(
|
||||
(
|
||||
float(bmax.x if ix else bmin.x),
|
||||
float(bmax.y if iy else bmin.y),
|
||||
float(bmax.z if iz else bmin.z),
|
||||
)
|
||||
)
|
||||
rotated = cage_rotation_3x3 @ (local - pivot_local) + pivot_local
|
||||
corners[(ix, iy, iz)] = mw @ rotated
|
||||
return corners
|
||||
|
||||
|
||||
def _abs_scale_matrix(mw: Any) -> Any:
|
||||
"""Return a copy of ``mw`` with all scale components ``abs()``-ed.
|
||||
|
||||
Without this, a negative-scale host produces a visible/clickable
|
||||
face inversion: ``mw @ local_vec`` flips the +axis face onto the
|
||||
-axis world side, while the rotation-only normal stays pointing
|
||||
in the +axis direction — so the gizmo for "the +X face" sits at
|
||||
world -X but reports its outward normal as +X.
|
||||
"""
|
||||
loc, rot, scale = mw.decompose()
|
||||
abs_scale = Vector((abs(scale.x), abs(scale.y), abs(scale.z)))
|
||||
return Matrix.LocRotScale(loc, rot, abs_scale)
|
||||
|
||||
|
||||
def _world_radius_to_screen_pixels(
|
||||
region: Any,
|
||||
rv3d: Any,
|
||||
center_world: Vector,
|
||||
world_radius: float,
|
||||
*,
|
||||
min_pixels: float = 0.0,
|
||||
) -> float:
|
||||
"""Return the on-screen pixel radius of a world-space circle.
|
||||
|
||||
Projects ``center_world`` and a sample point offset by
|
||||
``world_radius`` along the camera's view-aligned right axis to
|
||||
region pixels, and returns the screen-pixel distance between them.
|
||||
Falls back to ``min_pixels`` if either projection fails.
|
||||
"""
|
||||
try:
|
||||
view_inv = rv3d.view_matrix.inverted()
|
||||
right = Vector((view_inv[0][0], view_inv[0][1], view_inv[0][2])).normalized()
|
||||
except (AttributeError, ValueError):
|
||||
right = Vector((1.0, 0.0, 0.0))
|
||||
sample_world = center_world + right * world_radius
|
||||
return _world_segment_to_screen_pixels(region, rv3d, center_world, sample_world, min_pixels=min_pixels)
|
||||
|
||||
|
||||
def _world_segment_to_screen_pixels(
|
||||
region: Any,
|
||||
rv3d: Any,
|
||||
p0_world: Vector,
|
||||
p1_world: Vector,
|
||||
*,
|
||||
min_pixels: float = 0.0,
|
||||
) -> float:
|
||||
"""Return the on-screen pixel length of an arbitrary world segment.
|
||||
|
||||
Unlike :func:`_world_radius_to_screen_pixels`, this measures the
|
||||
ACTUAL projected length of the segment — foreshortening included.
|
||||
Use this when the segment direction is known to be oblique to the
|
||||
screen plane (e.g. a back face's outward normal): a perpendicular
|
||||
radius measurement overestimates the on-screen length, leaving
|
||||
halo strips visually narrower than the requested pixel target.
|
||||
"""
|
||||
p0 = location_3d_to_region_2d(region, rv3d, p0_world)
|
||||
p1 = location_3d_to_region_2d(region, rv3d, p1_world)
|
||||
if not p0 or not p1:
|
||||
return min_pixels
|
||||
dx = float(p1[0]) - float(p0[0])
|
||||
dy = float(p1[1]) - float(p0[1])
|
||||
return max(min_pixels, (dx * dx + dy * dy) ** 0.5)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gizmo classes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_GT_box_face_quad(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
|
||||
"""Near-invisible face-quad click target with drag-to-resize modal.
|
||||
|
||||
Geometry: a unit quad in the local XY plane at z=0. The adapter
|
||||
group's layout helper rotates and scales it onto the face plane;
|
||||
the quad is welded to the world face (``use_draw_scale = False``).
|
||||
"""
|
||||
|
||||
bl_idname = "BIM_GT_box_face_quad"
|
||||
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
|
||||
|
||||
__slots__ = (
|
||||
"custom_shape",
|
||||
"custom_shape_select",
|
||||
"init_value",
|
||||
"move_get_cb",
|
||||
"move_set_cb",
|
||||
"axis",
|
||||
"start_location",
|
||||
"depth_point",
|
||||
"callback",
|
||||
"ctrl_click_cb",
|
||||
"_group",
|
||||
"_face_axis",
|
||||
"is_max",
|
||||
"_drag_snapshot",
|
||||
"_last_geometry_state",
|
||||
"_strips_cache_key",
|
||||
)
|
||||
|
||||
def draw(self, context: Any) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: Any, select_id: int) -> None:
|
||||
# Back-facing quads bind ``custom_shape_select`` to the halo-strip
|
||||
# TRIS so clicks OUTSIDE the box silhouette catch the back face.
|
||||
# Front-facing quads leave it None and reuse ``custom_shape``.
|
||||
shape = getattr(self, "custom_shape_select", None) or self.custom_shape
|
||||
self.draw_custom_shape(shape, select_id=select_id)
|
||||
|
||||
def setup(self) -> None:
|
||||
if not hasattr(self, "custom_shape_"):
|
||||
self.custom_shape = self.new_custom_shape("TRIS", _QUAD_TRIS)
|
||||
self.custom_shape_select = None
|
||||
# Quad welded to world geometry — clicks must align with the
|
||||
# visible face, not a screen-size widget. Disables Blender's
|
||||
# per-frame pixel-constant autoscale.
|
||||
self.use_draw_scale = False
|
||||
|
||||
# ---- modal -------------------------------------------------------------
|
||||
|
||||
def invoke(self, context: Any, event: Any) -> set[str]:
|
||||
# CTRL+click handoff: dispatch a host-defined callback (e.g.
|
||||
# align-view) instead of starting a drag.
|
||||
if event.ctrl and getattr(self, "ctrl_click_cb", None) is not None:
|
||||
self.ctrl_click_cb(context, event)
|
||||
return {"FINISHED"}
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
if region is None or rv3d is None:
|
||||
return {"CANCELLED"}
|
||||
self.init_value = self.move_get_cb()
|
||||
# Freeze the projection plane at invoke — projection-plane
|
||||
# drift on tilted axes causes exponential delta runaway.
|
||||
self.depth_point = self.matrix_basis.translation.copy()
|
||||
self.start_location = region_2d_to_location_3d(region, rv3d, (event.mouse_x, event.mouse_y), self.depth_point)
|
||||
|
||||
if getattr(self, "_group", None) is not None:
|
||||
self._group._lock_for(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def exit(self, context: Any, cancel: bool) -> None:
|
||||
try:
|
||||
if context.area:
|
||||
context.area.header_text_set(None)
|
||||
if cancel:
|
||||
self.move_set_cb(self.init_value)
|
||||
if hasattr(self, "callback"):
|
||||
self.callback(self.move_get_cb())
|
||||
finally:
|
||||
self._drag_snapshot = None
|
||||
if getattr(self, "_group", None) is not None:
|
||||
self._group._unlock_all()
|
||||
|
||||
def modal(self, context: Any, event: Any, tweak: set[str]) -> set[str]:
|
||||
if event.type == "ESC":
|
||||
return {"CANCELLED"}
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
if region is None or rv3d is None:
|
||||
return {"CANCELLED"}
|
||||
end_location = region_2d_to_location_3d(region, rv3d, (event.mouse_x, event.mouse_y), self.depth_point)
|
||||
delta = (end_location - self.start_location).dot(self.axis)
|
||||
if "SNAP" in tweak:
|
||||
delta = round(delta, 1)
|
||||
if "PRECISE" in tweak:
|
||||
delta /= 10.0
|
||||
self.move_set_cb(self.init_value + delta)
|
||||
if context.area:
|
||||
context.area.header_text_set(f"Value: {self.move_get_cb():.3f} ({delta:.3f})")
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class BIM_GT_box_face_outline(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
|
||||
"""Thin non-interactive colored edge outline for one face.
|
||||
|
||||
Drawn as 4 line segments in the face plane. The layout helper
|
||||
toggles its ``alpha`` between near-zero and ``1.0`` based on the
|
||||
sibling face-quad's ``is_highlight`` state — so hovering the quad
|
||||
lights up the matching outline. ``hide_select = True`` keeps the
|
||||
outline out of the GPU selection buffer.
|
||||
"""
|
||||
|
||||
bl_idname = "BIM_GT_box_face_outline"
|
||||
bl_target_properties = ()
|
||||
|
||||
__slots__ = (
|
||||
"custom_shape",
|
||||
"_face_axis",
|
||||
"is_max",
|
||||
"_last_outline_state",
|
||||
)
|
||||
|
||||
def draw(self, context: Any) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: Any, select_id: int) -> None:
|
||||
return None
|
||||
|
||||
def setup(self) -> None:
|
||||
if not hasattr(self, "custom_shape_"):
|
||||
self.custom_shape = self.new_custom_shape("LINES", _QUAD_OUTLINE_LINES)
|
||||
self.use_draw_scale = False
|
||||
self.hide_select = True
|
||||
self._last_outline_state = "unit"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-redraw orchestrator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def apply_face_quad_layout(
|
||||
*,
|
||||
quad_gizmos,
|
||||
outline_gizmos,
|
||||
bmin: Any,
|
||||
bmax: Any,
|
||||
matrix_world: Any,
|
||||
cage_rotation: Any,
|
||||
region: Any,
|
||||
rv3d: Any,
|
||||
locked: bool,
|
||||
) -> None:
|
||||
"""Lay out 6 face quads + 6 outlines on the box for this redraw.
|
||||
|
||||
``quad_gizmos`` / ``outline_gizmos`` are length-6 sequences in
|
||||
:data:`FACE_ROUTES` order. ``bmin`` / ``bmax`` are the box corners
|
||||
in the host's local frame; ``matrix_world`` is the host's world
|
||||
matrix; ``cage_rotation`` is the OBB rotation as a 4x4 (use
|
||||
``Matrix.Identity(4)`` when rotation rides in ``matrix_world``).
|
||||
``region`` / ``rv3d`` drive the view-dependent front/back split and
|
||||
the screen-constant halo margin; passing ``rv3d = None`` bails.
|
||||
|
||||
Negative scale on the host is normalized to positive internally so
|
||||
the visible cube and the clickable face gizmos stay aligned —
|
||||
callers don't need to pre-process ``matrix_world``.
|
||||
|
||||
When ``locked`` (a drag is active), ``hide`` / ``select_bias``
|
||||
writes are skipped — the active quad's geometry is still refreshed
|
||||
so it tracks the moving box.
|
||||
"""
|
||||
if rv3d is None or getattr(rv3d, "view_rotation", None) is None:
|
||||
return
|
||||
if len(quad_gizmos) != 6 or len(outline_gizmos) != 6:
|
||||
return
|
||||
|
||||
mw = _abs_scale_matrix(matrix_world)
|
||||
mw_rot = mw.to_quaternion().to_matrix()
|
||||
mw_rot_scale = mw.to_3x3()
|
||||
cage_rotation_3x3 = cage_rotation.to_3x3()
|
||||
pivot_local = (bmin + bmax) * 0.5
|
||||
box_center_local = pivot_local
|
||||
face_midpoints_local = {
|
||||
(0, False): Vector((float(bmin.x), box_center_local.y, box_center_local.z)),
|
||||
(0, True): Vector((float(bmax.x), box_center_local.y, box_center_local.z)),
|
||||
(1, False): Vector((box_center_local.x, float(bmin.y), box_center_local.z)),
|
||||
(1, True): Vector((box_center_local.x, float(bmax.y), box_center_local.z)),
|
||||
(2, False): Vector((box_center_local.x, box_center_local.y, float(bmin.z))),
|
||||
(2, True): Vector((box_center_local.x, box_center_local.y, float(bmax.z))),
|
||||
}
|
||||
|
||||
view_dir = (rv3d.view_rotation @ Vector((0.0, 0.0, -1.0))).normalized()
|
||||
view_dir_tuple = (float(view_dir.x), float(view_dir.y), float(view_dir.z))
|
||||
face_normals_world = []
|
||||
for route_axis, route_is_max in FACE_ROUTES:
|
||||
axis_local = Vector(face_outward_axis_local(route_axis, route_is_max))
|
||||
n_world = (mw_rot @ cage_rotation_3x3 @ axis_local).normalized()
|
||||
face_normals_world.append((float(n_world.x), float(n_world.y), float(n_world.z)))
|
||||
front = front_facing_face_mask(tuple(face_normals_world), view_dir_tuple)
|
||||
|
||||
box_center_world = mw @ pivot_local
|
||||
corners_world = _compute_box_corners_world(bmin, bmax, pivot_local, cage_rotation_3x3, mw)
|
||||
route_to_index = {route: i for i, route in enumerate(FACE_ROUTES)}
|
||||
|
||||
for i, route in enumerate(FACE_ROUTES):
|
||||
quad_gz = quad_gizmos[i]
|
||||
is_front = front[i]
|
||||
axis_b, is_max_b = route
|
||||
|
||||
# Place the colored OUTLINE on every face using the same composed
|
||||
# face matrix the front-facing solid quad uses. Hidden/shown via
|
||||
# alpha at the end of the pass.
|
||||
outline_orient = _AXIS_ORIENT[route]
|
||||
outline_face_world, _outline_axis = _compute_face_basis(
|
||||
mw,
|
||||
mw_rot,
|
||||
cage_rotation,
|
||||
pivot_local,
|
||||
face_midpoints_local[route],
|
||||
outline_orient,
|
||||
)
|
||||
ow, oh = _compute_face_quad_scale(bmin, bmax, axis_b, is_max_b)
|
||||
outline_gizmos[i].matrix_basis = _compose_face_matrix_basis(
|
||||
outline_face_world, mw_rot_scale, cage_rotation, outline_orient, ow, oh
|
||||
)
|
||||
|
||||
if is_front:
|
||||
if not locked:
|
||||
quad_gz.hide = False
|
||||
quad_gz.select_bias = _FACE_QUAD_FRONT_FACING_SELECT_BIAS
|
||||
orient = _AXIS_ORIENT[route]
|
||||
face_world, world_axis = _compute_face_basis(
|
||||
mw,
|
||||
mw_rot,
|
||||
cage_rotation,
|
||||
pivot_local,
|
||||
face_midpoints_local[route],
|
||||
orient,
|
||||
)
|
||||
w, h = _compute_face_quad_scale(bmin, bmax, axis_b, is_max_b)
|
||||
quad_gz.matrix_basis = _compose_face_matrix_basis(face_world, mw_rot_scale, cage_rotation, orient, w, h)
|
||||
quad_gz.axis = world_axis
|
||||
if getattr(quad_gz, "_last_geometry_state", None) != "solid":
|
||||
quad_gz.custom_shape = quad_gz.new_custom_shape("TRIS", _QUAD_TRIS)
|
||||
quad_gz.custom_shape_select = None
|
||||
quad_gz._last_geometry_state = "solid"
|
||||
continue
|
||||
|
||||
# Back-facing: anchor at the back face centre; build halo strips
|
||||
# in the planes of the adjacent FRONT faces, extruded outside
|
||||
# the silhouette toward this face's outward normal.
|
||||
face_world = mw @ (cage_rotation_3x3 @ (face_midpoints_local[route] - pivot_local) + pivot_local)
|
||||
quad_gz.matrix_basis = Matrix.Translation(face_world)
|
||||
quad_gz.axis = (mw_rot @ cage_rotation_3x3 @ Vector(face_outward_axis_local(axis_b, is_max_b))).normalized()
|
||||
|
||||
adjacent_front_routes = [
|
||||
(axis_a, is_max_a)
|
||||
for axis_a in range(3)
|
||||
if axis_a != axis_b
|
||||
for is_max_a in (False, True)
|
||||
if front[route_to_index[(axis_a, is_max_a)]]
|
||||
]
|
||||
# Per-face world margin: measure the screen-projected length of
|
||||
# ONE world unit along THIS face's outward normal. The world
|
||||
# margin that yields ~N pixels on screen is then ``N / length``.
|
||||
# Foreshortening on oblique faces shortens the projected step,
|
||||
# so the world step must grow to keep the strip the same width
|
||||
# on screen.
|
||||
face_world_margin = 0.0
|
||||
if region is not None:
|
||||
sample_end = box_center_world + quad_gz.axis * 1.0
|
||||
screen_step = _world_segment_to_screen_pixels(region, rv3d, box_center_world, sample_end, min_pixels=0.0)
|
||||
if screen_step > 0.0:
|
||||
face_world_margin = _FACE_QUAD_HALO_TARGET_PIXELS / screen_step
|
||||
if face_world_margin <= 0.0 or not adjacent_front_routes:
|
||||
if not locked:
|
||||
quad_gz.hide = True
|
||||
quad_gz.select_bias = _FACE_QUAD_HALO_FRAME_SELECT_BIAS
|
||||
if getattr(quad_gz, "_last_geometry_state", None) != "empty":
|
||||
quad_gz.custom_shape = quad_gz.new_custom_shape("TRIS", _EMPTY_TRIS)
|
||||
quad_gz.custom_shape_select = None
|
||||
quad_gz._last_geometry_state = "empty"
|
||||
continue
|
||||
|
||||
extrusion_world = quad_gz.axis * face_world_margin
|
||||
extrusion_local = (
|
||||
float(extrusion_world.x),
|
||||
float(extrusion_world.y),
|
||||
float(extrusion_world.z),
|
||||
)
|
||||
all_tris: list[tuple[float, float, float]] = []
|
||||
for axis_a, is_max_a in adjacent_front_routes:
|
||||
edge_keys = _shared_edge_corner_keys(axis_a, is_max_a, axis_b, is_max_b)
|
||||
if edge_keys is None:
|
||||
continue
|
||||
key0, key1 = edge_keys
|
||||
wp0 = corners_world[key0]
|
||||
wp1 = corners_world[key1]
|
||||
local_p0 = (
|
||||
float(wp0.x - face_world.x),
|
||||
float(wp0.y - face_world.y),
|
||||
float(wp0.z - face_world.z),
|
||||
)
|
||||
local_p1 = (
|
||||
float(wp1.x - face_world.x),
|
||||
float(wp1.y - face_world.y),
|
||||
float(wp1.z - face_world.z),
|
||||
)
|
||||
all_tris.extend(_build_strip_tris_relative(local_p0, local_p1, extrusion_local))
|
||||
|
||||
if not locked:
|
||||
quad_gz.hide = False
|
||||
quad_gz.select_bias = _FACE_QUAD_HALO_FRAME_SELECT_BIAS
|
||||
|
||||
corner_keys = _face_corner_keys(axis_b, is_max_b)
|
||||
wc_local = [
|
||||
(
|
||||
float(corners_world[k].x - face_world.x),
|
||||
float(corners_world[k].y - face_world.y),
|
||||
float(corners_world[k].z - face_world.z),
|
||||
)
|
||||
for k in corner_keys
|
||||
]
|
||||
face_quad_local = [
|
||||
wc_local[0],
|
||||
wc_local[1],
|
||||
wc_local[2],
|
||||
wc_local[0],
|
||||
wc_local[2],
|
||||
wc_local[3],
|
||||
]
|
||||
if _strips_geometry_changed(quad_gz, tuple(face_quad_local), tuple(all_tris)):
|
||||
quad_gz.custom_shape = quad_gz.new_custom_shape("TRIS", face_quad_local)
|
||||
quad_gz.custom_shape_select = quad_gz.new_custom_shape("TRIS", all_tris)
|
||||
quad_gz._last_geometry_state = "strips"
|
||||
|
||||
# Outline alpha follows ONLY the hovered quad's own state — light
|
||||
# the outline of the face under the cursor, nothing else.
|
||||
if not locked:
|
||||
for outline_gz, quad_gz in zip(outline_gizmos, quad_gizmos, strict=True):
|
||||
lit = bool(getattr(quad_gz, "is_highlight", False))
|
||||
outline_gz.alpha = 1.0 if lit else 0.0
|
||||
outline_gz.alpha_highlight = 1.0 if lit else 0.0
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AXIS_COLOR",
|
||||
"FACE_QUAD_ALPHA",
|
||||
"FACE_QUAD_ALPHA_HIGHLIGHT",
|
||||
"FACE_QUAD_SELECT_BIAS",
|
||||
"FACE_ROUTES",
|
||||
"BIM_GT_box_face_outline",
|
||||
"BIM_GT_box_face_quad",
|
||||
"apply_face_quad_layout",
|
||||
"compute_face_resize",
|
||||
"face_outward_axis_local",
|
||||
"front_facing_face_mask",
|
||||
"view_axis_parallel_face_mask",
|
||||
]
|
||||
@@ -1,312 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 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/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Interactive face-quad resize gizmos for the active clip box.
|
||||
|
||||
Adapter group that binds the generic :mod:`face_quad` core to a Bonsai
|
||||
clip-box Empty: six near-invisible click quads + six edge outlines on
|
||||
the cube's faces. Dragging a face does a ONE-SIDED resize — the dragged
|
||||
face moves along its outward world normal while the opposite face stays
|
||||
put — by writing the empty's ``location`` and ``scale``. Bonsai's
|
||||
depsgraph handler then re-arms the clip planes from the new matrix.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
from . import face_quad
|
||||
|
||||
# Local-frame bounds of the empty's CUBE display. The display spans
|
||||
# ``[-empty_display_size, +empty_display_size]^3``; Bonsai always sets
|
||||
# ``empty_display_size = 1.0`` on clip-box hosts, so the local box is
|
||||
# the unit cube. The empty's per-axis scale + rotation + translation
|
||||
# ride in ``matrix_world``, which the layout helper applies.
|
||||
_LOCAL_BMIN = Vector((-1.0, -1.0, -1.0))
|
||||
_LOCAL_BMAX = Vector((1.0, 1.0, 1.0))
|
||||
|
||||
|
||||
def _world_axis(empty: bpy.types.Object, axis: int, is_max: bool) -> Vector:
|
||||
"""Outward world-space unit normal of the ``(axis, is_max)`` face.
|
||||
|
||||
Uses the rotation-only matrix so a negative-scale empty doesn't
|
||||
flip the resulting direction — the visible "+X face" then stays
|
||||
associated with world +X (transformed through rotation).
|
||||
"""
|
||||
rot_mat = empty.matrix_world.to_quaternion().to_matrix()
|
||||
n = Vector(rot_mat.col[axis])
|
||||
if n.length <= 0.0:
|
||||
return Vector((0.0, 0.0, 0.0))
|
||||
n.normalize()
|
||||
return n if is_max else -n
|
||||
|
||||
|
||||
def _world_half_extent(empty: bpy.types.Object, axis: int) -> float:
|
||||
"""The empty's box half-extent along local ``axis`` in WORLD units.
|
||||
|
||||
A CUBE empty's local cube is ``±empty_display_size``; ``matrix_world``
|
||||
stretches it by the column length on ``axis``. So the world
|
||||
half-extent is ``|column[axis]| * empty_display_size``.
|
||||
"""
|
||||
col_len = empty.matrix_world.to_3x3().col[axis].length
|
||||
display_size = abs(float(getattr(empty, "empty_display_size", 1.0) or 1.0))
|
||||
return float(col_len) * display_size
|
||||
|
||||
|
||||
def _make_face_get_cb(gz: Any, group: Any, axis: int, is_max: bool):
|
||||
"""Closure returning the world half-extent at drag start and
|
||||
snapshotting the empty's full transform on the gizmo instance.
|
||||
|
||||
The snapshot lives on the gizmo (not the group) so a PERSISTENT
|
||||
group servicing multiple clip boxes can't bleed one drag's state
|
||||
onto another. Cleared on ``exit`` by the shared face-quad hook.
|
||||
"""
|
||||
|
||||
def getter() -> float:
|
||||
empty = group._empty
|
||||
if empty is None:
|
||||
return 0.0
|
||||
existing = getattr(gz, "_drag_snapshot", None)
|
||||
if existing is not None and existing.get("empty_name") == getattr(empty, "name", None):
|
||||
return float(existing["world_half"])
|
||||
|
||||
world_half = _world_half_extent(empty, axis)
|
||||
display_size = abs(float(getattr(empty, "empty_display_size", 1.0) or 1.0))
|
||||
gz._drag_snapshot = {
|
||||
"empty_name": getattr(empty, "name", None),
|
||||
"world_half": world_half,
|
||||
"location": tuple(float(v) for v in empty.location),
|
||||
"scale": tuple(float(v) for v in empty.scale),
|
||||
"display_size": display_size if display_size != 0.0 else 1.0,
|
||||
"world_axis": tuple(_world_axis(empty, axis, is_max)),
|
||||
}
|
||||
return float(world_half)
|
||||
|
||||
return getter
|
||||
|
||||
|
||||
def _make_ctrl_click_cb(axis: int, is_max: bool):
|
||||
"""Closure that dispatches CTRL+click on a face to the align-view operator.
|
||||
|
||||
Routing through an operator (rather than mutating ``rv3d`` here)
|
||||
keeps the action F3-searchable and undoable.
|
||||
"""
|
||||
|
||||
def _callback(_context: Any, _event: Any) -> None:
|
||||
bpy.ops.bim.align_view_to_clip_face("INVOKE_DEFAULT", axis=axis, is_max=is_max)
|
||||
|
||||
return _callback
|
||||
|
||||
|
||||
def _make_face_set_cb(gz: Any, group: Any, axis: int, is_max: bool):
|
||||
"""Closure that applies a one-sided face resize by writing the
|
||||
empty's ``location`` + ``scale``.
|
||||
|
||||
The modal calls this with ``value = init + delta`` where ``delta``
|
||||
is the cursor's projection onto the face's OUTWARD world normal.
|
||||
Both reads come from ``gz._drag_snapshot`` so every frame is
|
||||
relative to drag start, never compounding.
|
||||
"""
|
||||
del is_max # snapshot's world_axis carries the direction
|
||||
|
||||
def setter(value: float) -> None:
|
||||
empty = group._empty
|
||||
if empty is None:
|
||||
return
|
||||
snap = getattr(gz, "_drag_snapshot", None)
|
||||
if snap is None or snap.get("empty_name") != getattr(empty, "name", None):
|
||||
return
|
||||
|
||||
new_scale_axis, new_location = face_quad.compute_face_resize(
|
||||
value=value,
|
||||
init_world_half=snap["world_half"],
|
||||
init_location=snap["location"],
|
||||
world_axis=snap["world_axis"],
|
||||
display_size=snap["display_size"],
|
||||
)
|
||||
new_scale = list(snap["scale"])
|
||||
# Preserve the sign of the original scale so a user-flipped empty
|
||||
# stays flipped after the resize — compute_face_resize returns a
|
||||
# positive magnitude, the sign is the user's intent to keep.
|
||||
sign = -1.0 if snap["scale"][axis] < 0.0 else 1.0
|
||||
new_scale[axis] = sign * new_scale_axis
|
||||
|
||||
empty.scale = new_scale
|
||||
empty.location = Vector(new_location)
|
||||
|
||||
return setter
|
||||
|
||||
|
||||
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup): # noqa: N801 — Blender bl_idname convention
|
||||
"""Face-quad resize handles on the active clip box.
|
||||
|
||||
Renders six near-invisible click-target quads and six colored edge
|
||||
outlines on the active clip-box empty whenever clipping is enabled.
|
||||
Click-and-drag a face to resize one-sided; the opposite face stays
|
||||
put. CTRL+click and plain click fall through to selection.
|
||||
"""
|
||||
|
||||
bl_idname = "OBJECT_GGT_bim_clip_box"
|
||||
bl_label = "Bonsai Clip Box Faces"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Any) -> bool:
|
||||
scene = getattr(context, "scene", None)
|
||||
if scene is None:
|
||||
return False
|
||||
scene_props = tool.ClipBox.get_scene_props(scene)
|
||||
if not scene_props.enabled or not scene_props.enable_gizmos:
|
||||
return False
|
||||
active_clip_box = tool.ClipBox.get_active_clip_box(scene)
|
||||
if active_clip_box is None:
|
||||
return False
|
||||
# Only render when the user has the active clip box itself
|
||||
# selected — otherwise the face handles would intercept clicks
|
||||
# meant for the geometry behind them.
|
||||
return getattr(context, "active_object", None) is active_clip_box
|
||||
|
||||
@classmethod
|
||||
def setup_keymap(cls, keyconfig):
|
||||
# Bind CLICK_DRAG so plain LEFTMOUSE PRESS passes through to
|
||||
# selection — the user can still click through a near-invisible
|
||||
# face quad to pick a mesh behind it.
|
||||
km = keyconfig.keymaps.new(
|
||||
name=cls.bl_idname,
|
||||
space_type=cls.bl_space_type,
|
||||
region_type=cls.bl_region_type,
|
||||
)
|
||||
km.keymap_items.new("gizmogroup.gizmo_tweak", type="LEFTMOUSE", value="CLICK_DRAG")
|
||||
km.keymap_items.new("gizmogroup.gizmo_tweak", type="LEFTMOUSE", value="PRESS", ctrl=True)
|
||||
return km
|
||||
|
||||
def setup(self, context: Any) -> None:
|
||||
# ``_empty`` is resolved each refresh so the PERSISTENT group
|
||||
# follows whichever clip box is active in the scene PG.
|
||||
self._empty: bpy.types.Object | None = None
|
||||
self._locked = False
|
||||
self._face_routes: list[tuple[int, bool]] = []
|
||||
|
||||
for axis, is_max in face_quad.FACE_ROUTES:
|
||||
gz = self.gizmos.new(face_quad.BIM_GT_box_face_quad.bl_idname)
|
||||
gz._group = self
|
||||
gz._face_axis = axis
|
||||
gz.is_max = is_max
|
||||
gz._drag_snapshot = None
|
||||
gz._last_geometry_state = "solid"
|
||||
gz._strips_cache_key = None
|
||||
gz.color = face_quad.AXIS_COLOR[axis]
|
||||
gz.color_highlight = tuple(min(1.0, c + 0.3) for c in face_quad.AXIS_COLOR[axis])
|
||||
gz.alpha = face_quad.FACE_QUAD_ALPHA
|
||||
gz.alpha_highlight = face_quad.FACE_QUAD_ALPHA_HIGHLIGHT
|
||||
gz.use_draw_modal = True
|
||||
gz.scale_basis = 1.0
|
||||
gz.select_bias = face_quad.FACE_QUAD_SELECT_BIAS
|
||||
gz.move_get_cb = _make_face_get_cb(gz, self, axis, is_max)
|
||||
gz.move_set_cb = _make_face_set_cb(gz, self, axis, is_max)
|
||||
# CTRL+click on a face aligns the viewport to look at it.
|
||||
gz.ctrl_click_cb = _make_ctrl_click_cb(axis, is_max)
|
||||
self._face_routes.append((axis, is_max))
|
||||
|
||||
# Outlines added last so they composite on top of the quad
|
||||
# fills (Blender draws gizmos in creation order).
|
||||
for axis, is_max in face_quad.FACE_ROUTES:
|
||||
ol = self.gizmos.new(face_quad.BIM_GT_box_face_outline.bl_idname)
|
||||
ol._face_axis = axis
|
||||
ol.is_max = is_max
|
||||
ol.color = face_quad.AXIS_COLOR[axis]
|
||||
ol.color_highlight = face_quad.AXIS_COLOR[axis]
|
||||
ol.alpha = 0.0
|
||||
ol.alpha_highlight = 0.0
|
||||
ol.line_width = 2.5
|
||||
|
||||
def _quad_gizmos(self):
|
||||
return self.gizmos[: len(self._face_routes)]
|
||||
|
||||
def _outline_gizmos(self):
|
||||
n = len(self._face_routes)
|
||||
return self.gizmos[n : 2 * n]
|
||||
|
||||
def refresh(self, context: Any) -> None:
|
||||
"""State-change path: resolve the active empty, then run the
|
||||
shared face-quad layout so the quads aren't stale for a frame
|
||||
after a selection or active-index change."""
|
||||
empty = tool.ClipBox.get_active_clip_box(context.scene)
|
||||
self._empty = empty
|
||||
if empty is None:
|
||||
for gz in self.gizmos:
|
||||
gz.hide = True
|
||||
return
|
||||
self._layout(context, empty)
|
||||
|
||||
def draw_prepare(self, context: Any) -> None:
|
||||
"""Per-redraw — fires on orbit — re-run the layout so the
|
||||
front/back split, halo strips, and outline highlights track
|
||||
the camera and any live G/R/S on the empty."""
|
||||
empty = self._empty
|
||||
if empty is None:
|
||||
return
|
||||
self._layout(context, empty)
|
||||
|
||||
def _layout(self, context: Any, empty: bpy.types.Object) -> None:
|
||||
face_quad.apply_face_quad_layout(
|
||||
quad_gizmos=self._quad_gizmos(),
|
||||
outline_gizmos=self._outline_gizmos(),
|
||||
bmin=_LOCAL_BMIN,
|
||||
bmax=_LOCAL_BMAX,
|
||||
matrix_world=empty.matrix_world,
|
||||
# The empty's rotation rides in matrix_world, so the
|
||||
# box-local OBB rotation is identity.
|
||||
cage_rotation=Matrix.Identity(4),
|
||||
region=getattr(context, "region", None),
|
||||
rv3d=getattr(context, "region_data", None),
|
||||
locked=self._locked,
|
||||
)
|
||||
|
||||
# ---- mutual exclusion (lock siblings during a drag) ------------------
|
||||
|
||||
def _lock_for(self, active_gizmo) -> None:
|
||||
self._locked = True
|
||||
for gz in self.gizmos:
|
||||
if gz is not active_gizmo:
|
||||
with contextlib.suppress(ReferenceError, RuntimeError):
|
||||
gz.hide = True
|
||||
|
||||
def _unlock_all(self) -> None:
|
||||
self._locked = False
|
||||
for gz in self.gizmos:
|
||||
with contextlib.suppress(ReferenceError, RuntimeError):
|
||||
gz.hide = False
|
||||
# Rebuild caps synchronously so the cross-section overlay
|
||||
# re-forms the instant the user releases the handle, rather
|
||||
# than waiting for the depsgraph's debounced rebuild path.
|
||||
with contextlib.suppress(RuntimeError, ReferenceError):
|
||||
tool.ClipBox.rebuild_caps_now()
|
||||
# Push an undo step so the user can revert a face drag with Ctrl+Z.
|
||||
with contextlib.suppress(RuntimeError):
|
||||
bpy.ops.ed.undo_push(message="Resize Clip Box")
|
||||
@@ -1,294 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 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/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
|
||||
from . import data
|
||||
|
||||
# NOTE: do NOT add ``from __future__ import annotations`` to this module.
|
||||
# PEP 563 stringifies the operator's EnumProperty class annotations, which
|
||||
# breaks any introspection that reads ``cls.__annotations__[name].keywords``
|
||||
# — including the enum-search helper that draws the search-button icon.
|
||||
|
||||
CLIP_BOX_NAME = "ClipBox"
|
||||
|
||||
# Display labels for the source-based picker, used for the menu entries and
|
||||
# the dialog title. The dict keys are the canonical source-kind identifiers.
|
||||
SOURCE_KIND_LABELS: dict[str, str] = {
|
||||
"SPATIAL": "Spatial Element",
|
||||
"CLASS": "Class",
|
||||
"TYPE": "Type",
|
||||
"MATERIAL": "Material",
|
||||
"PROFILE": "Profile",
|
||||
"DRAWING": "Drawing",
|
||||
"STATUS": "Status",
|
||||
"SYSTEM": "System",
|
||||
"GROUP": "Group",
|
||||
"ZONE": "Zone",
|
||||
}
|
||||
|
||||
|
||||
_SOURCE_ID_DISPATCH = {
|
||||
"SPATIAL": data.spatial_items,
|
||||
"CLASS": data.class_items,
|
||||
"TYPE": data.type_items,
|
||||
"MATERIAL": data.material_items,
|
||||
"PROFILE": data.profile_items,
|
||||
"DRAWING": data.drawing_items,
|
||||
"STATUS": data.status_items,
|
||||
"SYSTEM": data.system_items,
|
||||
"GROUP": data.group_items,
|
||||
"ZONE": data.zone_items,
|
||||
}
|
||||
|
||||
|
||||
def _source_id_items(self, context):
|
||||
"""Dispatch the ``source_id`` enum items based on the picked ``source_kind``."""
|
||||
fn = _SOURCE_ID_DISPATCH.get(self.source_kind)
|
||||
if fn is None:
|
||||
return [(data.NO_OPTIONS_ID, "No options", "")]
|
||||
return fn(self, context)
|
||||
|
||||
|
||||
def _source_display_name(kind, source_id):
|
||||
"""Human-readable name of the picked source, used in the clip-box name."""
|
||||
if kind == "STATUS":
|
||||
return next((label for value, label in data.STATUS_LABELS if value == source_id), source_id)
|
||||
if kind == "CLASS":
|
||||
# source_id IS the human-readable IFC class name.
|
||||
return source_id
|
||||
ifc = tool.Ifc.get()
|
||||
if ifc is None:
|
||||
return source_id
|
||||
try:
|
||||
entity = ifc.by_id(int(source_id))
|
||||
except (TypeError, ValueError, RuntimeError):
|
||||
return source_id
|
||||
return (getattr(entity, "Name", None) or "Unnamed").strip() or "Unnamed"
|
||||
|
||||
|
||||
class BIM_OT_align_view_to_clip_face(bpy.types.Operator):
|
||||
bl_idname = "bim.align_view_to_clip_face"
|
||||
bl_label = "Align View to Clip Box Face"
|
||||
bl_description = "Orient the 3D viewport to look directly at the picked clip-box face"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
axis: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"})
|
||||
is_max: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
rv3d = getattr(context, "region_data", None)
|
||||
if rv3d is None:
|
||||
return {"CANCELLED"}
|
||||
clip_box = tool.ClipBox.get_active_clip_box(context.scene)
|
||||
if clip_box is None:
|
||||
return {"CANCELLED"}
|
||||
rot_mat = clip_box.matrix_world.to_quaternion().to_matrix()
|
||||
outward_local = Vector((0.0, 0.0, 0.0))
|
||||
outward_local[self.axis] = 1.0 if self.is_max else -1.0
|
||||
outward = (rot_mat @ outward_local).normalized()
|
||||
if outward.length == 0.0:
|
||||
return {"CANCELLED"}
|
||||
up_world = (rot_mat @ _local_up_for_face(self.axis, self.is_max)).normalized()
|
||||
rv3d.view_rotation = _view_rotation_from_forward_and_up(-outward, up_world)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def _local_up_for_face(axis: int, is_max: bool) -> Vector:
|
||||
"""Box-local up direction for a face, following Blender numpad conventions.
|
||||
|
||||
Side faces (local ±X / ±Y normal) → local +Z is up. Top face (local +Z
|
||||
normal) → local +Y is up; bottom face (local -Z normal) → local -Y is
|
||||
up. The caller rotates this through the empty's matrix so the
|
||||
resulting world up axis tracks the box's orientation.
|
||||
"""
|
||||
if axis == 2:
|
||||
return Vector((0.0, 1.0, 0.0)) if is_max else Vector((0.0, -1.0, 0.0))
|
||||
return Vector((0.0, 0.0, 1.0))
|
||||
|
||||
|
||||
def _view_rotation_from_forward_and_up(forward: Vector, up_hint: Vector) -> "bpy.types.Quaternion":
|
||||
"""Build a camera ``view_rotation`` that looks along ``forward`` with
|
||||
``up_hint`` projected to the camera's local +Y."""
|
||||
back = -forward.normalized()
|
||||
right = up_hint.cross(back)
|
||||
if right.length < 1e-6:
|
||||
right = Vector((1.0, 0.0, 0.0))
|
||||
right.normalize()
|
||||
up = back.cross(right).normalized()
|
||||
return Matrix(
|
||||
(
|
||||
(right.x, up.x, back.x),
|
||||
(right.y, up.y, back.y),
|
||||
(right.z, up.z, back.z),
|
||||
)
|
||||
).to_quaternion()
|
||||
|
||||
|
||||
class BIM_OT_add_clip_box(bpy.types.Operator):
|
||||
bl_idname = "bim.add_clip_box"
|
||||
bl_label = "Add Clip Box"
|
||||
bl_description = (
|
||||
"Create a clip box empty at the 3D cursor. The empty's location, rotation, and scale "
|
||||
"drive the viewport clip planes; resize with S, move with G, rotate with R"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
# Default to a 20m cube (scale 10 around [-1, +1] local cube) so
|
||||
# the volume covers a typical building storey or two rather than
|
||||
# the meaningless 2m unit cube. The user resizes with S.
|
||||
matrix = Matrix.Translation(context.scene.cursor.location.copy()) @ Matrix.Diagonal((10.0, 10.0, 10.0, 1.0))
|
||||
tool.ClipBox.create_clip_box_empty(context, matrix, name=CLIP_BOX_NAME)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_add_clip_box_for_source(bpy.types.Operator):
|
||||
bl_idname = "bim.add_clip_box_for_source"
|
||||
bl_label = "Add Clip Box From Source"
|
||||
bl_description = (
|
||||
"Create a clip box sized to a chosen source: a spatial container, IFC type, material, "
|
||||
"profile, drawing camera frustum, element status, system, group, or zone"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
source_kind: bpy.props.EnumProperty(
|
||||
name="Source Kind",
|
||||
items=[(kind, label, "") for kind, label in SOURCE_KIND_LABELS.items()],
|
||||
default="SPATIAL",
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
source_id: bpy.props.EnumProperty(
|
||||
name="Source",
|
||||
items=_source_id_items,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
label = f"Clip {SOURCE_KIND_LABELS.get(self.source_kind, 'Source')}"
|
||||
# Search button appears once the enum exceeds the helper's threshold,
|
||||
# giving the user a popup picker instead of a plain dropdown.
|
||||
prop_with_search(layout, self, "source_id", text=label)
|
||||
|
||||
def execute(self, context):
|
||||
if not self.source_id or self.source_id == data.NO_OPTIONS_ID:
|
||||
self.report({"ERROR"}, "No source selected.")
|
||||
return {"CANCELLED"}
|
||||
matrix = tool.ClipBox.compute_matrix_for_source(self.source_kind, self.source_id)
|
||||
if matrix is None:
|
||||
kind_label = SOURCE_KIND_LABELS.get(self.source_kind, self.source_kind)
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"No elements found for {kind_label} '{_source_display_name(self.source_kind, self.source_id)}'.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
name = f"ClipBox.{SOURCE_KIND_LABELS.get(self.source_kind, self.source_kind)}.{_source_display_name(self.source_kind, self.source_id)}"
|
||||
tool.ClipBox.create_clip_box_empty(context, matrix, name=name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_remove_clip_box(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_clip_box"
|
||||
bl_label = "Remove Clip Box"
|
||||
bl_description = "Remove this clip box and its host empty"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
index: bpy.props.IntProperty(default=-1, options={"SKIP_SAVE"})
|
||||
delete_object: bpy.props.BoolProperty(default=True, name="Delete Host Object")
|
||||
|
||||
def execute(self, context):
|
||||
scene_props = tool.ClipBox.get_scene_props(context.scene)
|
||||
index = self.index if self.index >= 0 else scene_props.active_clip_box_index
|
||||
if index < 0 or index >= len(scene_props.clip_boxes):
|
||||
return {"CANCELLED"}
|
||||
|
||||
entry = scene_props.clip_boxes[index]
|
||||
obj = entry.obj
|
||||
scene_props.clip_boxes.remove(index)
|
||||
if scene_props.active_clip_box_index >= len(scene_props.clip_boxes):
|
||||
scene_props.active_clip_box_index = max(0, len(scene_props.clip_boxes) - 1)
|
||||
|
||||
if self.delete_object and obj is not None:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
tool.ClipBox.refresh(context.scene)
|
||||
tool.ClipBox.save_to_project_pset(context.scene)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_set_active_clip_box(bpy.types.Operator):
|
||||
bl_idname = "bim.set_active_clip_box"
|
||||
bl_label = "Set Active Clip Box"
|
||||
bl_description = "Set this clip box as the active one driving the viewport clip"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
index: bpy.props.IntProperty(default=-1, options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
scene_props = tool.ClipBox.get_scene_props(context.scene)
|
||||
if self.index < 0 or self.index >= len(scene_props.clip_boxes):
|
||||
return {"CANCELLED"}
|
||||
scene_props.active_clip_box_index = self.index
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_toggle_clip_box_enabled(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_clip_box_enabled"
|
||||
bl_label = "Toggle Clip Box"
|
||||
bl_description = "Toggle whether the active clip box is driving the viewport clip planes"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
scene_props = tool.ClipBox.get_scene_props(context.scene)
|
||||
scene_props.enabled = not scene_props.enabled
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_duplicate_clip_box(bpy.types.Operator):
|
||||
bl_idname = "bim.duplicate_clip_box"
|
||||
bl_label = "Duplicate Clip Box"
|
||||
bl_description = "Duplicate this clip box: copy its empty + matrix into a new entry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
index: bpy.props.IntProperty(default=-1, options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
scene_props = tool.ClipBox.get_scene_props(context.scene)
|
||||
source_index = self.index if self.index >= 0 else scene_props.active_clip_box_index
|
||||
if source_index < 0 or source_index >= len(scene_props.clip_boxes):
|
||||
return {"CANCELLED"}
|
||||
source = scene_props.clip_boxes[source_index].obj
|
||||
if source is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
copy = tool.ClipBox.create_clip_box_empty(context, source.matrix_world.copy(), name=source.name)
|
||||
# Preserve the source's display attrs so the duplicate matches.
|
||||
copy.empty_display_type = source.empty_display_type
|
||||
copy.empty_display_size = source.empty_display_size
|
||||
copy.show_in_front = source.show_in_front
|
||||
return {"FINISHED"}
|
||||
@@ -1,165 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 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/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.prop import ObjProperty
|
||||
|
||||
|
||||
class BIMClipBoxProperties(PropertyGroup):
|
||||
"""Per-object marker for a clip-box host empty.
|
||||
|
||||
The host empty's ``matrix_world`` is the single source of truth for
|
||||
the clip box's pose and dimensions: translation = box centre,
|
||||
rotation = box orientation, per-axis scale = world half-extents. The
|
||||
visible cube comes from the empty's CUBE display.
|
||||
|
||||
Only ``is_clip_box`` lives here; visibility (``enabled``) and overlay
|
||||
(``show_caps``) are global per-file and live on the Scene PG.
|
||||
"""
|
||||
|
||||
is_clip_box: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
description="True when this empty was created as a clip-box host. Internal flag; not user-edited.",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_clip_box: bool
|
||||
|
||||
|
||||
def update_active_clip_box_index(self, context):
|
||||
tool.ClipBox.schedule_refresh()
|
||||
tool.ClipBox.select_active_clip_box(context)
|
||||
# Rebuild caps for the new active box's clip volume.
|
||||
tool.ClipBox.invalidate_cap_cache(immediate=True)
|
||||
|
||||
|
||||
def update_show_caps(self, context):
|
||||
tool.ClipBox.schedule_refresh()
|
||||
# Off → on must trigger a rebuild so caps reappear immediately rather
|
||||
# than wait for the next depsgraph tick. The rebuild is a no-op when
|
||||
# show_caps is now False (it clears and returns), so this is safe in
|
||||
# both directions.
|
||||
tool.ClipBox.invalidate_cap_cache()
|
||||
|
||||
|
||||
def update_enabled(self, context):
|
||||
tool.ClipBox.schedule_refresh()
|
||||
|
||||
|
||||
def update_clip_only_ifc_products(self, context):
|
||||
# The eligibility set for capping changed — drop the cache and let the
|
||||
# debounced rebuild pick up the new objects on the next idle tick.
|
||||
tool.ClipBox.invalidate_cap_cache()
|
||||
|
||||
|
||||
def update_include_linked_ifc(self, context):
|
||||
tool.ClipBox.invalidate_cap_cache()
|
||||
|
||||
|
||||
class BIMSceneClipBoxProperties(PropertyGroup):
|
||||
"""Scene-level registry of clip boxes in this file.
|
||||
|
||||
Multiple boxes may exist; ``active_clip_box_index`` selects which one
|
||||
drives the viewport clip at any time. ``enabled`` and ``show_caps``
|
||||
are global because the user's intent ("hide everything outside the
|
||||
box", "draw cap overlays") applies file-wide, not per box.
|
||||
|
||||
``enabled`` is intentionally not persisted to the project pset:
|
||||
opening a fresh IFC should never silently hide geometry behind a
|
||||
remembered toggle. Selecting any clip-box empty in the viewport
|
||||
re-arms it (see :meth:`tool.ClipBox._sync_active_to_selection`).
|
||||
"""
|
||||
|
||||
clip_boxes: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
active_clip_box_index: bpy.props.IntProperty(
|
||||
default=0,
|
||||
min=0,
|
||||
update=update_active_clip_box_index,
|
||||
description="Index of the clip box currently driving the viewport clip planes",
|
||||
)
|
||||
enabled: bpy.props.BoolProperty(
|
||||
name="Enabled",
|
||||
default=False,
|
||||
update=update_enabled,
|
||||
description="When enabled, the active clip box hides all viewport geometry outside its 6 faces",
|
||||
)
|
||||
show_caps: bpy.props.BoolProperty(
|
||||
name="Show Caps",
|
||||
default=True,
|
||||
update=update_show_caps,
|
||||
description=(
|
||||
"Draw filled cross-section caps where IFC product geometry "
|
||||
"crosses the active clip planes. Disable for performance on "
|
||||
"very heavy scenes"
|
||||
),
|
||||
)
|
||||
# Stored on the Scene PG so Blender persists it in the .blend; deliberately
|
||||
# NOT written to the project pset so the IFC stays portable across users
|
||||
# who may have different Blender-side reference geometry to clip.
|
||||
clip_only_ifc_products: bpy.props.BoolProperty(
|
||||
name="Only IFC Products",
|
||||
default=True,
|
||||
update=update_clip_only_ifc_products,
|
||||
description=(
|
||||
"When enabled, only IFC element geometry gets cross-section caps. "
|
||||
"Disable to also cap Blender-side reference meshes (sketches, "
|
||||
"imported obj, primitive cubes, …)"
|
||||
),
|
||||
)
|
||||
# Opt-in inclusion of geometry sitting inside loaded Project › Links
|
||||
# collection-instance empties. Off by default — linked IFCs commonly
|
||||
# carry the entire site / structural / MEP context, and bisecting
|
||||
# them on every clip-box edit can be expensive.
|
||||
include_linked_ifc: bpy.props.BoolProperty(
|
||||
name="Include Linked IFC",
|
||||
default=False,
|
||||
update=update_include_linked_ifc,
|
||||
description=(
|
||||
"Also generate cross-section caps for geometry inside linked "
|
||||
"IFC files (Project ▸ Links). Off by default — linked IFCs may "
|
||||
"carry the entire site / structural backbone, and capping them "
|
||||
"adds per-mesh bisect cost on every clip-box edit"
|
||||
),
|
||||
)
|
||||
# Also Scene-only — gizmo visibility is a per-user editing preference,
|
||||
# not a portable IFC property.
|
||||
enable_gizmos: bpy.props.BoolProperty(
|
||||
name="Show Face Handles",
|
||||
default=True,
|
||||
description=(
|
||||
"Show interactive face-resize handles on the active clip box. "
|
||||
"Disable to fall back to plain G/R/S transforms on the empty"
|
||||
),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
active_clip_box_index: int
|
||||
enabled: bool
|
||||
show_caps: bool
|
||||
clip_only_ifc_products: bool
|
||||
include_linked_ifc: bool
|
||||
enable_gizmos: bool
|
||||
@@ -1,145 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026 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/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bpy.types import Menu, Panel, UIList
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
# Per-kind icon for the source-picker menu. Picked from Blender's built-in
|
||||
# icon set; semantically close to the kind so users can scan the menu visually.
|
||||
_SOURCE_MENU_ENTRIES: tuple[tuple[str, str, str], ...] = (
|
||||
("SPATIAL", "Clip Spatial Element", "OUTLINER_COLLECTION"),
|
||||
("CLASS", "Clip by Class", "BLANK1"),
|
||||
("TYPE", "Clip Type", "FILE_3D"),
|
||||
("MATERIAL", "Clip Material", "MATERIAL"),
|
||||
("PROFILE", "Clip Profile", "MESH_CIRCLE"),
|
||||
("DRAWING", "Clip Drawing Extents", "CAMERA_DATA"),
|
||||
("STATUS", "Clip by Status", "INFO"),
|
||||
("SYSTEM", "Clip by System", "MOD_FLUID"),
|
||||
("GROUP", "Clip by Group", "OUTLINER_OB_GROUP_INSTANCE"),
|
||||
("ZONE", "Clip by Zone", "MOD_LATTICE"),
|
||||
)
|
||||
|
||||
|
||||
class BIM_MT_clip_box_add_for_source(Menu):
|
||||
bl_idname = "BIM_MT_clip_box_add_for_source"
|
||||
bl_label = "Add Clip Box From Source"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for kind, label, icon in _SOURCE_MENU_ENTRIES:
|
||||
op = layout.operator("bim.add_clip_box_for_source", text=label, icon=icon)
|
||||
op.source_kind = kind
|
||||
|
||||
|
||||
class BIM_MT_clip_box_settings(Menu):
|
||||
bl_idname = "BIM_MT_clip_box_settings"
|
||||
bl_label = "Clip Box Settings"
|
||||
|
||||
def draw(self, context):
|
||||
scene_props = tool.ClipBox.get_scene_props(context.scene)
|
||||
self.layout.prop(scene_props, "clip_only_ifc_products")
|
||||
self.layout.prop(scene_props, "include_linked_ifc")
|
||||
self.layout.prop(scene_props, "enable_gizmos")
|
||||
|
||||
|
||||
class BIM_MT_clip_box_info(Menu):
|
||||
bl_idname = "BIM_MT_clip_box_info"
|
||||
bl_label = "Clip Box Face Handles"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Face Handles", icon="INFO")
|
||||
layout.separator()
|
||||
layout.label(text="Drag a face to resize the clip box on that axis.")
|
||||
layout.label(text="The opposite face stays fixed (one-sided resize).")
|
||||
layout.label(text="Ctrl+Click a face to align the viewport to it.")
|
||||
layout.separator()
|
||||
layout.label(text="Toggle handles from the Settings (gear) menu.")
|
||||
|
||||
|
||||
class BIM_UL_clip_box(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
|
||||
obj = item.obj
|
||||
row = layout.row(align=True)
|
||||
if obj is None:
|
||||
# Host empty was deleted from outliner; still expose the
|
||||
# remove button so the orphan entry isn't permanent.
|
||||
row.label(text="(missing)", icon="ERROR")
|
||||
row.operator("bim.remove_clip_box", text="", icon="X", emboss=False).index = index
|
||||
return
|
||||
row.prop(obj, "name", text="", emboss=False, icon="MESH_CUBE")
|
||||
row.operator("bim.duplicate_clip_box", text="", icon="DUPLICATE", emboss=False).index = index
|
||||
row.operator("bim.remove_clip_box", text="", icon="X", emboss=False).index = index
|
||||
|
||||
|
||||
class BIM_PT_clip_box(Panel):
|
||||
bl_idname = "BIM_PT_clip_box"
|
||||
bl_label = "Clip Box"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_parent_id = "BIM_PT_tab_sandbox"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
scene_props = tool.ClipBox.get_scene_props(context.scene)
|
||||
|
||||
toggles = layout.row(align=True)
|
||||
toggles.scale_y = 2.0
|
||||
toggles.prop(
|
||||
scene_props,
|
||||
"enabled",
|
||||
text="Enable Clipping",
|
||||
icon="HIDE_OFF" if scene_props.enabled else "HIDE_ON",
|
||||
toggle=True,
|
||||
)
|
||||
toggles.prop(scene_props, "show_caps", text="Show Caps", icon="MOD_SOLIDIFY", toggle=True)
|
||||
toggles.menu("BIM_MT_clip_box_settings", icon="PREFERENCES", text="")
|
||||
toggles.menu("BIM_MT_clip_box_info", icon="INFO", text="")
|
||||
|
||||
layout.separator()
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_clip_box", icon="ADD", text="Add Clip Box")
|
||||
row.menu("BIM_MT_clip_box_add_for_source", icon="DOWNARROW_HLT", text="")
|
||||
|
||||
layout.template_list(
|
||||
"BIM_UL_clip_box",
|
||||
"",
|
||||
scene_props,
|
||||
"clip_boxes",
|
||||
scene_props,
|
||||
"active_clip_box_index",
|
||||
rows=3,
|
||||
)
|
||||
|
||||
obj = tool.ClipBox.get_active_clip_box(context.scene)
|
||||
if obj is None:
|
||||
layout.label(text="No active clip box", icon="INFO")
|
||||
return
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Edit the empty with G / R / S to move / rotate / resize")
|
||||
col.prop(obj, "location")
|
||||
col.prop(obj, "rotation_euler")
|
||||
col.prop(obj, "scale")
|
||||
@@ -156,8 +156,6 @@ class CostSchedulesData:
|
||||
values = root_element.CostValues
|
||||
elif root_element.is_a("IfcConstructionResource"):
|
||||
values = root_element.BaseCosts
|
||||
else:
|
||||
assert False, root_element
|
||||
for cost_value in values or []:
|
||||
cls._load_cost_value(root_element, data, cost_value)
|
||||
# data["CostValues"].append(cost_value.id())
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -127,25 +127,36 @@ class ObjectDocumentData:
|
||||
identification = None
|
||||
|
||||
if is_information:
|
||||
identification = tool.Document.get_document_information_id(relating_document)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
identification = relating_document.DocumentId
|
||||
else:
|
||||
identification = relating_document.Identification
|
||||
|
||||
location = getattr(relating_document, "Location", None)
|
||||
description = getattr(relating_document, "Description", "No description")
|
||||
else:
|
||||
description = relating_document.Description
|
||||
referenced_document = tool.Document.get_reference_document(relating_document)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
reference_to_document = relating_document.ReferenceToDocument
|
||||
if not name and reference_to_document:
|
||||
name = reference_to_document[0].Name
|
||||
|
||||
if not name and referenced_document:
|
||||
name = referenced_document.Name
|
||||
identification = relating_document.ItemReference
|
||||
if not identification and reference_to_document:
|
||||
identification = reference_to_document[0].DocumentId
|
||||
location = relating_document.Location
|
||||
else:
|
||||
referenced_document = relating_document.ReferencedDocument
|
||||
if not name and referenced_document:
|
||||
name = referenced_document.Name
|
||||
|
||||
identification = tool.Document.get_external_reference_id(relating_document)
|
||||
if not identification and referenced_document:
|
||||
identification = tool.Document.get_document_information_id(referenced_document)
|
||||
identification = relating_document.Identification
|
||||
if not identification and referenced_document:
|
||||
identification = referenced_document.Identification
|
||||
|
||||
location = relating_document.Location
|
||||
# IFC2X3 IfcDocumentInformation has no Location to fall back to.
|
||||
if location is None and referenced_document and tool.Ifc.get_schema() != "IFC2X3":
|
||||
location = referenced_document.Location
|
||||
location = relating_document.Location
|
||||
if location is None and referenced_document:
|
||||
location = referenced_document.Location
|
||||
|
||||
location = cls.convert_to_file_uri(location) if location else None
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was modified with the assistance of an AI coding tool.
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -47,7 +45,6 @@ classes = (
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
operator.ConvertSVGToDXF,
|
||||
operator.CopyAnnotationToDrawing,
|
||||
operator.CopyTextToSelection,
|
||||
operator.CreateDrawing,
|
||||
operator.CreateSheets,
|
||||
@@ -139,34 +136,14 @@ classes = (
|
||||
gizmos.GizmoArrow2D,
|
||||
gizmos.GizmoCone,
|
||||
gizmos.GizmoDimension,
|
||||
gizmos.GizmoLockOpen,
|
||||
gizmos.GizmoLockClosed,
|
||||
gizmos.GizmoLock,
|
||||
gizmos.GizmoArc,
|
||||
gizmos.GizmoLinkToggle,
|
||||
gizmos.GizmoFillet,
|
||||
gizmos.GizmoWallCornerIcon,
|
||||
gizmos.GizmoWallTeeIcon,
|
||||
gizmos.GizmoPen,
|
||||
gizmos.GizmoValidate,
|
||||
gizmos.GizmoCancel,
|
||||
gizmos.GizmoPlus,
|
||||
gizmos.GizmoMinus,
|
||||
gizmos.GizmoTrash,
|
||||
gizmos.GizmoArrayParent,
|
||||
gizmos.GizmoArrayAll,
|
||||
gizmos.GizmoArrayLayerIndicator,
|
||||
gizmos.GizmoCountLabel,
|
||||
gizmos.GizmoMerge,
|
||||
gizmos.GizmoSplit,
|
||||
gizmos.GizmoUnjoin,
|
||||
gizmos.GizmoExtend,
|
||||
gizmos.GizmoExtendVertical,
|
||||
gizmos.GizmoOffsetExterior,
|
||||
gizmos.GizmoOffsetCenter,
|
||||
gizmos.GizmoOffsetInterior,
|
||||
gizmos.GizmoAddOpening,
|
||||
gizmos.GizmoCycle,
|
||||
gizmos.GizmoMenu,
|
||||
# Drawing-specific gizmos
|
||||
gizmos.UglyDotGizmo,
|
||||
gizmos.ExtrusionGuidesGizmo,
|
||||
|
||||
@@ -425,12 +425,10 @@ class BaseDecorator:
|
||||
|
||||
blf.size(font_id, font_size_px)
|
||||
|
||||
w, h = None, None
|
||||
if box_alignment or center or vcenter:
|
||||
w, h = blf.dimensions(font_id, text)
|
||||
|
||||
if box_alignment:
|
||||
assert w is not None and h is not None
|
||||
box_alignment_offset = Vector((0, 0))
|
||||
if "bottom" in box_alignment:
|
||||
pass
|
||||
@@ -452,12 +450,10 @@ class BaseDecorator:
|
||||
else:
|
||||
# horizontal centering
|
||||
if center:
|
||||
assert w is not None
|
||||
pos -= Vector((cos, sin)) * w * 0.5
|
||||
|
||||
# vertical centering
|
||||
if vcenter:
|
||||
assert h is not None
|
||||
pos -= Vector((-sin, cos)) * h * 0.5
|
||||
|
||||
# side-shifting
|
||||
@@ -1005,8 +1001,6 @@ class FallDecorator(BaseDecorator):
|
||||
O = A.copy()
|
||||
O.z = B.z
|
||||
run = (B - O).length
|
||||
|
||||
angle_tg = None
|
||||
if run != 0:
|
||||
angle_tg = rise / run
|
||||
angle = round(degrees(atan(angle_tg)))
|
||||
@@ -1024,7 +1018,6 @@ class FallDecorator(BaseDecorator):
|
||||
elif object_type == "SLOPE_PERCENT":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
assert angle_tg is not None
|
||||
return f"{round(angle_tg * 100)} %"
|
||||
return "NO DATA"
|
||||
|
||||
@@ -1256,7 +1249,6 @@ class SectionLevelDecorator(BaseDecorator):
|
||||
}
|
||||
|
||||
# process edges
|
||||
text_position, text_dir = None, None
|
||||
for edge in edges_original:
|
||||
v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]]
|
||||
start_i = len(output_verts)
|
||||
@@ -1562,39 +1554,32 @@ class SectionDecorator(BaseDecorator):
|
||||
v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]]
|
||||
start_i = len(output_verts)
|
||||
|
||||
circle_head = None
|
||||
if display_start_circle or display_end_circle:
|
||||
circle_head = get_circle_head(circle_size)
|
||||
|
||||
triangle_head, divider_offset, edge_dir_circle = None, None, None
|
||||
display_symbol = display_start_symbol or display_end_symbol
|
||||
if display_symbol or connect_markers:
|
||||
if display_start_symbol or display_end_symbol or connect_markers:
|
||||
edge_dir = (v1 - v0).normalized()
|
||||
side = (edge_dir.yx * Vector((1, -1))).to_3d()
|
||||
edge_dir_circle = edge_dir * circle_size
|
||||
|
||||
if display_symbol:
|
||||
triangle_head = get_triangle_head(edge_dir, -side, triangle_length, triangle_width)
|
||||
divider_offset = []
|
||||
divider_offset.append(edge_dir_circle if connect_markers else edge_dir_circle * 3)
|
||||
divider_offset.append(edge_dir_circle)
|
||||
if display_start_symbol or display_end_symbol:
|
||||
triangle_head = get_triangle_head(edge_dir, -side, triangle_length, triangle_width)
|
||||
divider_offset = []
|
||||
divider_offset.append(edge_dir_circle if connect_markers else edge_dir_circle * 3)
|
||||
divider_offset.append(edge_dir_circle)
|
||||
|
||||
if display_start_circle:
|
||||
assert circle_head is not None
|
||||
start_i = add_verts_sequence([v + v0 for v in circle_head], start_i, **out_kwargs, closed=True)
|
||||
# circle middle divider
|
||||
if not display_start_symbol:
|
||||
assert divider_offset is not None
|
||||
start_i = add_verts_sequence(
|
||||
[v0 + divider_offset[0], v0 - divider_offset[1]], start_i, **out_kwargs
|
||||
)
|
||||
|
||||
if display_start_symbol:
|
||||
assert triangle_head is not None
|
||||
start_i = add_verts_sequence([v + v0 for v in triangle_head], start_i, **out_kwargs, closed=True)
|
||||
|
||||
if display_end_circle:
|
||||
assert circle_head is not None
|
||||
start_i = add_verts_sequence([v + v1 for v in circle_head], start_i, **out_kwargs, closed=True)
|
||||
# circle middle divider
|
||||
if not display_end_symbol:
|
||||
@@ -1603,11 +1588,9 @@ class SectionDecorator(BaseDecorator):
|
||||
)
|
||||
|
||||
if display_end_symbol:
|
||||
assert triangle_head is not None
|
||||
start_i = add_verts_sequence([v + v1 for v in triangle_head], start_i, **out_kwargs, closed=True)
|
||||
|
||||
if connect_markers:
|
||||
assert edge_dir_circle is not None
|
||||
gap = []
|
||||
gap.append(edge_dir_circle if display_start_symbol else Vector((0, 0, 0)))
|
||||
gap.append(edge_dir_circle if display_end_symbol else Vector((0, 0, 0)))
|
||||
@@ -1694,12 +1677,6 @@ class CutDecorator:
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
self.fallback_colour = (0.3, 0.3, 0.3, 1)
|
||||
|
||||
# Evaluate camera movement once per redraw rather than twice per object: is_camera_moved()
|
||||
# runs eval()/numpy on the camera matrix and, as a side effect, refreshes the stored
|
||||
# checksum on the first True result - so calling it per object also made the second call
|
||||
# (fill) see an already-updated checksum and skip recalculating when it shouldn't.
|
||||
self.camera_moved = self.is_camera_moved()
|
||||
|
||||
all_vertices = []
|
||||
all_edges = []
|
||||
selected_vertices = []
|
||||
@@ -1810,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))
|
||||
@@ -1825,35 +1802,23 @@ class CutDecorator:
|
||||
|
||||
# Currently selected objects must be recalculated as they may be being moved / edited.
|
||||
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
||||
is_selected = obj.select_get()
|
||||
recalc_cut = not has_cut_cache or is_selected or self.camera_moved
|
||||
recalc_fill = not has_fill_cache or is_selected or self.camera_moved
|
||||
if not (recalc_cut or recalc_fill):
|
||||
return
|
||||
|
||||
# The intersection test builds a bmesh and scans every vertex; both recalculations need
|
||||
# the same answer, so compute it once here rather than once in each.
|
||||
is_intersecting = tool.Drawing.is_intersecting_camera(obj, context.scene.camera)
|
||||
if recalc_cut:
|
||||
self.recalculate_cut(context, obj, element, is_intersecting)
|
||||
if recalc_fill:
|
||||
self.recalculate_fill(context, obj, element, is_intersecting)
|
||||
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_cut(context, obj, element)
|
||||
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(
|
||||
self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance, is_intersecting: bool
|
||||
) -> None:
|
||||
if is_intersecting:
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
if tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
|
||||
DecoratorData.cut_cache[element.id()] = (verts, edges)
|
||||
else:
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
|
||||
def recalculate_fill(
|
||||
self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance, is_intersecting: bool
|
||||
) -> None:
|
||||
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
element_id = element.id()
|
||||
|
||||
if not is_intersecting:
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.fill_cache[element_id] = {}
|
||||
return
|
||||
|
||||
@@ -1906,8 +1871,6 @@ class CutDecorator:
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
else:
|
||||
assert False, material
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
material = layer_set.MaterialLayers[0].Material
|
||||
@@ -1934,8 +1897,6 @@ class CutDecorator:
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
else:
|
||||
assert False, usage
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,9 +50,6 @@ def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
|
||||
if camera.type != props.camera_type:
|
||||
camera.type = props.camera_type
|
||||
|
||||
if props.update_props and (drawing := tool.Ifc.get_entity(camera_obj)):
|
||||
tool.Drawing.sync_perspective_camera_shifts(drawing, camera)
|
||||
|
||||
ortho_scale, aspect_ratio = props.get_scale_and_aspect_ratio()
|
||||
scene_render = scene.render
|
||||
if (camera.ortho_scale != ortho_scale) or not tool.Cad.is_x(
|
||||
|
||||
@@ -189,20 +189,14 @@ def format_distance(
|
||||
if hasattr(length_unit, "Prefix") and length_unit.Prefix:
|
||||
unit_length = length_unit.Prefix + length_unit.Name
|
||||
unit_length_mapping = {
|
||||
"MILE": "MILES",
|
||||
"FOOT": "FEET",
|
||||
"INCH": "INCHES",
|
||||
"KILOMETRE": "KILOMETERS",
|
||||
"METRE": "METERS",
|
||||
"DECIMETRE": "DECIMETERS",
|
||||
"CENTIMETRE": "CENTIMETERS",
|
||||
"MILLIMETRE": "MILLIMETERS",
|
||||
"MICROMETRE": "MICROMETERS",
|
||||
}
|
||||
# Fall through for units without a dedicated formatter (e.g.
|
||||
# HECTOMETRE) so they use the adaptive branch instead of a
|
||||
# KeyError (#8255).
|
||||
unit_length = unit_length_mapping.get(unit_length, unit_length)
|
||||
unit_length = unit_length_mapping[unit_length]
|
||||
# For now we only format area in IFC Units
|
||||
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
|
||||
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
|
||||
@@ -225,11 +219,9 @@ def format_distance(
|
||||
unit_system, unit_length, unit_fraction = unit_mapping[custom_unit]
|
||||
|
||||
value *= unit_scale
|
||||
tx_dist = None
|
||||
|
||||
# Imperial Formatting
|
||||
if unit_system == "IMPERIAL":
|
||||
toInches = None
|
||||
if in_unit_length:
|
||||
if unit_length == "INCHES":
|
||||
toInches = 1
|
||||
@@ -243,7 +235,6 @@ def format_distance(
|
||||
toInches = 1550
|
||||
inPerFoot = 144
|
||||
|
||||
assert toInches is not None
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
@@ -322,7 +313,7 @@ def format_distance(
|
||||
if not feet and not add_inches:
|
||||
tx_dist += str(feet) + "'"
|
||||
|
||||
if not feet and add_inches and unit_length != "INCHES":
|
||||
if not feet and add_inches:
|
||||
if value < 0:
|
||||
tx_dist += "-0' - "
|
||||
else:
|
||||
@@ -386,7 +377,6 @@ def format_distance(
|
||||
if precision and isinstance(precision, float):
|
||||
value = precision * round(float(value) / precision)
|
||||
|
||||
fmt = None
|
||||
if decimal_places is not None:
|
||||
fmt = "%1." + str(decimal_places) + "f"
|
||||
|
||||
@@ -466,10 +456,8 @@ def format_distance(
|
||||
tx_dist = fmt % d_cm
|
||||
|
||||
else:
|
||||
assert f"Unexpected unit_system - '{unit_system}'."
|
||||
# tx_dist = fmt % value
|
||||
tx_dist = fmt % value
|
||||
|
||||
assert tx_dist is not None
|
||||
return tx_dist
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ import shapely
|
||||
from bpy_extras.image_utils import load_image
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from lxml import etree
|
||||
from mathutils import Color, Matrix, Vector
|
||||
from mathutils import Color, Vector
|
||||
|
||||
import bonsai.bim.export_ifc
|
||||
import bonsai.bim.handler
|
||||
@@ -228,90 +228,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
def get_copy_annotation_target_drawings(self, context):
|
||||
global COPY_ANNOTATION_TARGET_DRAWINGS_ENUM
|
||||
drawings = [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]
|
||||
drawings.sort(key=lambda d: d.Name or "")
|
||||
COPY_ANNOTATION_TARGET_DRAWINGS_ENUM = [(str(d.id()), d.Name or "Unnamed", "") for d in drawings]
|
||||
return COPY_ANNOTATION_TARGET_DRAWINGS_ENUM
|
||||
|
||||
|
||||
COPY_ANNOTATION_TARGET_DRAWINGS_ENUM = []
|
||||
|
||||
|
||||
class CopyAnnotationToDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.copy_annotation_to_drawing"
|
||||
bl_label = "Copy Annotation To Drawing"
|
||||
bl_description = (
|
||||
"Copy the selected annotations to another drawing.\n\n"
|
||||
"The copies become independent annotations assigned to the chosen drawing, "
|
||||
"placed in its view plane. The originals stay in their current drawing"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
target_drawing: bpy.props.EnumProperty(name="Target Drawing", items=get_copy_annotation_target_drawings)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
target_drawing: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC project loaded.")
|
||||
return False
|
||||
if not cls.get_selected_annotations(context):
|
||||
cls.poll_message_set("No annotation selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_selected_annotations(cls, context) -> list[ifcopenshell.entity_instance]:
|
||||
return [
|
||||
element
|
||||
for obj in context.selected_objects
|
||||
if (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcAnnotation")
|
||||
and element.ObjectType != "DRAWING"
|
||||
]
|
||||
|
||||
def invoke(self, context, event):
|
||||
assert context.window_manager
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
row = self.layout.row()
|
||||
row.prop(self, "target_drawing")
|
||||
|
||||
def _execute(self, context):
|
||||
if not self.target_drawing:
|
||||
self.report({"ERROR"}, "No target drawing selected.")
|
||||
return {"CANCELLED"}
|
||||
target_drawing = tool.Ifc.get().by_id(int(self.target_drawing))
|
||||
annotations = self.get_selected_annotations(context)
|
||||
previous_selection = [obj for a in annotations if (obj := tool.Ifc.get_object(a))]
|
||||
previous_active = context.view_layer.objects.active
|
||||
copied = core.copy_annotations_to_drawing(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Drawing,
|
||||
tool.Geometry,
|
||||
annotations=annotations,
|
||||
target_drawing=target_drawing,
|
||||
)
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
for obj in previous_selection:
|
||||
if obj.name in context.view_layer.objects:
|
||||
obj.select_set(True)
|
||||
if previous_active and previous_active.name in context.view_layer.objects:
|
||||
context.view_layer.objects.active = previous_active
|
||||
skipped = len(annotations) - len(copied)
|
||||
message = f"Copied {len(copied)} annotations to {target_drawing.Name or 'Unnamed'}."
|
||||
if skipped:
|
||||
message += f" Skipped {skipped} already in that drawing."
|
||||
self.report({"INFO"}, message)
|
||||
|
||||
|
||||
class CreateDrawing(bpy.types.Operator):
|
||||
"""Creates/refreshes a .svg drawing
|
||||
|
||||
@@ -330,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,
|
||||
@@ -686,7 +602,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
context_type: Literal["body", "annotation"],
|
||||
drawing_elements: set[ifcopenshell.entity_instance],
|
||||
target_view: str,
|
||||
link_matrix: Optional[Matrix] = None,
|
||||
) -> None:
|
||||
drawing_elements = drawing_elements.copy()
|
||||
contexts_: list[list[int]] = getattr(contexts, context_type)
|
||||
@@ -698,19 +613,9 @@ class CreateDrawing(bpy.types.Operator):
|
||||
geom_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
geom_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
|
||||
|
||||
is_plan = ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view
|
||||
z_offset = (0.002 if target_view == "PLAN_VIEW" else -0.002) if is_plan else 0.0
|
||||
|
||||
if link_matrix is not None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc)
|
||||
t = link_matrix.to_translation()
|
||||
offset = (t.x / unit_scale, t.y / unit_scale, t.z / unit_scale + z_offset)
|
||||
geom_settings.set("model-offset", offset)
|
||||
q = link_matrix.to_quaternion()
|
||||
geom_settings.set("model-rotation", (q.x, q.y, q.z, q.w))
|
||||
elif z_offset:
|
||||
if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
|
||||
# A 2mm Z offset to combat Z-fighting in plan or RCPs
|
||||
geom_settings.set("model-offset", (0.0, 0.0, z_offset))
|
||||
geom_settings.set("model-offset", (0.0, 0.0, 0.002 if target_view == "PLAN_VIEW" else -0.002))
|
||||
|
||||
geom_settings.set("context-ids", context)
|
||||
it = ifcopenshell.geom.iterator(
|
||||
@@ -782,8 +687,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
else:
|
||||
assert False, material
|
||||
|
||||
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
||||
|
||||
@@ -808,6 +711,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
|
||||
bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges)
|
||||
|
||||
prev_co = None
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
@@ -824,8 +728,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
else:
|
||||
assert False, usage
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
@@ -993,10 +895,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
||||
return svg_path
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
semantics = None
|
||||
pairs = None
|
||||
|
||||
# in case of printing multiple drawings we need to sync just once
|
||||
if self.sync and self.drawing_index == 0:
|
||||
with profile("sync"):
|
||||
@@ -1025,16 +923,11 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
# Map ifc_path → (ifc_file, link_matrix); main file has no link_matrix (None)
|
||||
files: dict[str, tuple[ifcopenshell.file, Optional[Matrix]]] = {bim_props.ifc_file: (tool.Ifc.get(), None)}
|
||||
files = {bim_props.ifc_file: tool.Ifc.get()}
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
for link in props.get_loaded_links_for_drawings():
|
||||
try:
|
||||
link_matrix = tool.Project.calculate_link_matrix(link)
|
||||
except Exception:
|
||||
link_matrix = None
|
||||
files[link.filepath] = (self.get_linked_file(link), link_matrix)
|
||||
files[link.filepath] = self.get_linked_file(link)
|
||||
|
||||
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
|
||||
self.setup_serialiser(target_view)
|
||||
@@ -1042,13 +935,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
tree = ifcopenshell.geom.tree()
|
||||
tree.enable_face_styles(True)
|
||||
|
||||
# Accumulated across every file in the loop below (main model plus any
|
||||
# linked models) so the SHAPELY fill pass after the loop covers all of
|
||||
# them, not just whichever file happened to be processed last.
|
||||
raycast_objs = set()
|
||||
elements_with_faces = set()
|
||||
|
||||
for ifc_path, (ifc, link_matrix) in files.items():
|
||||
for ifc_path, ifc in files.items():
|
||||
# Don't use draw.main() just whilst we're prototyping and experimenting
|
||||
# TODO: hash paths are never used
|
||||
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
|
||||
@@ -1057,24 +944,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.serialiser.setFile(ifc)
|
||||
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
|
||||
|
||||
if self.cprops.fill_mode == "SHAPELY":
|
||||
for element in drawing_elements.copy():
|
||||
if element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj and obj.type == "MESH" and len(obj.data.polygons):
|
||||
elements_with_faces.add(element.GlobalId)
|
||||
raycast_objs.add(obj)
|
||||
|
||||
# Get all representation contexts to see what we're dealing with.
|
||||
# Drawings only draw bodies and annotations (and facetation, due to a Revit bug).
|
||||
# A drawing prioritises a target view context first, followed by a model view context as a fallback.
|
||||
# Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised.
|
||||
contexts = self.get_linework_contexts(ifc, target_view)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view, link_matrix)
|
||||
self.serialize_contexts_elements(
|
||||
ifc, tree, contexts, "annotation", drawing_elements, target_view, link_matrix
|
||||
)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view)
|
||||
|
||||
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
|
||||
with profile("Camera element"):
|
||||
@@ -1141,6 +1017,16 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# shapely variant
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
|
||||
raycast_objs = set()
|
||||
elements_with_faces = set()
|
||||
for element in drawing_elements.copy():
|
||||
if element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj and obj.type == "MESH" and len(obj.data.polygons):
|
||||
elements_with_faces.add(element.GlobalId)
|
||||
raycast_objs.add(obj)
|
||||
|
||||
projections = root.xpath(
|
||||
".//svg:g[contains(@class, 'projection')]", namespaces={"svg": "http://www.w3.org/2000/svg"}
|
||||
)
|
||||
@@ -1400,18 +1286,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.svg_settings = ifcopenshell.geom.settings()
|
||||
self.svg_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
self.svg_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
|
||||
# SVG edge classification (issue #3668). See edge-classification.md. Settings are
|
||||
# per-drawing, stored in EPset_Drawing and read into self.cprops by import_camera_props.
|
||||
try:
|
||||
self.svg_settings.set("svg-use-edge-classification", self.cprops.use_edge_classification)
|
||||
self.svg_settings.set("svg-render-crease-edges", self.cprops.render_creases)
|
||||
self.svg_settings.set("svg-valley-angle-min-degrees", self.cprops.valley_angle_min_degrees)
|
||||
self.svg_settings.set("svg-render-sharp-edges", self.cprops.render_sharp)
|
||||
self.svg_settings.set("svg-ridge-angle-min-degrees", self.cprops.ridge_angle_min_degrees)
|
||||
self.svg_settings.set("svg-emit-flush-edges", self.cprops.render_flush)
|
||||
except Exception:
|
||||
# Backwards compatibility with older ifcopenshell builds that don't expose these keys.
|
||||
pass
|
||||
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
|
||||
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(
|
||||
@@ -1552,18 +1426,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
"/Pset_.*Common/.Status",
|
||||
"EPset_Status.Status",
|
||||
"EPset_Status.UserDefinedStatus",
|
||||
"Material.Name",
|
||||
]
|
||||
|
||||
join_classes = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinClasses")
|
||||
if join_classes:
|
||||
join_classes = tuple(c.strip() for c in join_classes.split(",") if c.strip())
|
||||
else:
|
||||
# Architectural convention only merges these objects by default. E.g. pipe
|
||||
# segments and fittings shouldn't merge. Users may override this per-drawing
|
||||
# via the EPset_Drawing.JoinClasses property (e.g. to also join IfcCovering).
|
||||
join_classes = ("IfcWall", "IfcSlab")
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
joined_paths = {}
|
||||
self.is_manifold_cache = {}
|
||||
@@ -1665,7 +1529,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
)
|
||||
path.attrib["d"] = d
|
||||
|
||||
if not any(element.is_a(c) for c in join_classes):
|
||||
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
|
||||
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
|
||||
continue
|
||||
|
||||
keys = []
|
||||
@@ -1817,12 +1682,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
key=lambda a: (
|
||||
tool.Drawing.get_annotation_z_index(a),
|
||||
1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0,
|
||||
# Deterministic tiebreaker so equal-priority annotations keep a
|
||||
# stable order across sessions. Without it the order comes from
|
||||
# the set union above, which depends on entity hashes (and thus
|
||||
# the file pointer), shuffling annotations between Blender
|
||||
# restarts. See #6608.
|
||||
a.id(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2459,51 +2318,29 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
bl_description = (
|
||||
"Activates the selected drawing view.\n\n"
|
||||
+ "ALT+CLICK to keep the viewport position.\n\n"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
|
||||
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views, "
|
||||
+ "then select their cameras (the first selected drawing's camera becomes active).\n\n"
|
||||
+ "SHIFT+CTRL+ALT+CLICK to do the same but also select the annotations, not just the cameras"
|
||||
+ "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,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
load_selected_annotations: bpy.props.BoolProperty(
|
||||
name="Load Selected Annotations",
|
||||
description="Load the annotations of all selected drawings without switching the active view.",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
include_annotations_in_selection: bpy.props.BoolProperty(
|
||||
name="Include Annotations In Selection",
|
||||
description="Also select the loaded annotation objects, not just the drawing cameras.",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
drawing: int
|
||||
should_view_from_camera: bool
|
||||
use_quick_preview: bool
|
||||
load_selected_annotations: bool
|
||||
include_annotations_in_selection: bool
|
||||
|
||||
def invoke(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
|
||||
if event.type == "LEFTMOUSE" and event.shift and event.ctrl:
|
||||
self.load_selected_annotations = True
|
||||
if event.alt:
|
||||
self.include_annotations_in_selection = True
|
||||
return self.execute(context)
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
self.should_view_from_camera = False
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
@@ -2516,37 +2353,6 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
if props.is_editing_drawings == False:
|
||||
bpy.ops.bim.load_drawings()
|
||||
|
||||
if self.load_selected_annotations:
|
||||
objs_to_select = []
|
||||
active_camera = None
|
||||
for d in props.drawings:
|
||||
if not (d.is_drawing and d.is_selected):
|
||||
continue
|
||||
selected_drawing = tool.Ifc.get().by_id(d.ifc_definition_id)
|
||||
# Importing the camera (if missing) ensures the drawing's
|
||||
# collection exists so the annotations get collected into it.
|
||||
if not (camera := tool.Ifc.get_object(selected_drawing)):
|
||||
camera = tool.Drawing.import_drawing(selected_drawing)
|
||||
group = tool.Drawing.get_drawing_group(selected_drawing)
|
||||
tool.Drawing.import_annotations_in_group(group)
|
||||
|
||||
if active_camera is None:
|
||||
active_camera = camera
|
||||
objs_to_select.append(camera)
|
||||
if self.include_annotations_in_selection:
|
||||
for element in tool.Drawing.get_group_elements(group) or []:
|
||||
if element.is_a("IfcAnnotation") and element.ObjectType != "DRAWING":
|
||||
if annotation_obj := tool.Ifc.get_object(element):
|
||||
objs_to_select.append(annotation_obj)
|
||||
|
||||
# Select the checked drawings' objects, with the first drawing's camera as active.
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in objs_to_select:
|
||||
obj.select_set(True)
|
||||
if active_camera is not None:
|
||||
context.view_layer.objects.active = active_camera
|
||||
return {"FINISHED"}
|
||||
|
||||
drawing = tool.Ifc.get().by_id(self.drawing)
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
|
||||
@@ -2632,10 +2438,7 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
|
||||
bl_description = (
|
||||
"Activates the selected drawing view.\n\n"
|
||||
+ "ALT+CLICK to keep the viewport position.\n\n"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
|
||||
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views, "
|
||||
+ "then select their cameras (the first selected drawing's camera becomes active).\n\n"
|
||||
+ "SHIFT+CTRL+ALT+CLICK to do the same but also select the annotations, not just the cameras"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view"
|
||||
)
|
||||
|
||||
|
||||
@@ -3502,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"}
|
||||
|
||||
|
||||
@@ -3561,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):
|
||||
@@ -3697,7 +3503,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if sheet.is_a("IfcDocumentInformation"):
|
||||
self.document_type = "SHEET"
|
||||
self.name = sheet.Name
|
||||
self.identification = tool.Document.get_document_information_id(sheet)
|
||||
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
|
||||
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
|
||||
self.document_type = "TITLEBLOCK"
|
||||
else:
|
||||
@@ -3831,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
|
||||
@@ -4079,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)
|
||||
@@ -4369,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
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user