mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b3967609c | |||
| 9a7cd5b373 | |||
| 701635b81c | |||
| 88f6c7cf65 | |||
| cbdf1a193e | |||
| 95048ea2dd | |||
| 0e1a2510ff | |||
| fff653b882 | |||
| 160348df1f | |||
| 278a35e729 | |||
| e1bf717af5 | |||
| be30f9cbea | |||
| 4971546de9 | |||
| d9a3c2d5c2 | |||
| 5066fc5a29 | |||
| 9041857277 | |||
| 7e93a73c6a | |||
| 99640912c1 | |||
| 798ed0d502 | |||
| c3f325861c | |||
| 8433999575 | |||
| bba1ff3786 | |||
| a39e4c552a | |||
| 21c6384b7e | |||
| 07ef382c7e | |||
| 65af40e7d5 | |||
| cfe30a505e | |||
| 04677c1b80 | |||
| f06873d24e | |||
| cd6e531120 | |||
| f9f0f1915f | |||
| e0a1577ae0 | |||
| 33a4639de5 | |||
| a3038528bb | |||
| eea060b72d | |||
| 862697baae | |||
| 9a3ae628aa | |||
| 46da629c8b | |||
| fd85b29777 | |||
| 95952d03f8 | |||
| 5ad40612fb | |||
| 6294c35a11 | |||
| de2a7c9527 | |||
| a37de44a24 | |||
| c0b7256832 | |||
| 42db172827 | |||
| f52e95a05a | |||
| 04ad0b92bd | |||
| 8416ca46a7 | |||
| 10ce30b2ac | |||
| 00c971fa2d | |||
| b86bf45167 | |||
| 22fd318d21 |
+2
-3
@@ -1,8 +1,7 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/0.24.0/gersemi/configuration.schema.json
|
||||
|
||||
# Gersemi doesn't support autodetection of macros/functions from other files or from the current one
|
||||
# and requires to explicitly list directories/cmake files that define them.
|
||||
definitions: ["./cmake", "./src"]
|
||||
# Needed for gersemi to detect custom functions and macros.
|
||||
definitions: ["./cmake"]
|
||||
disable_formatting: false
|
||||
extensions: []
|
||||
indent: 4
|
||||
|
||||
@@ -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()
|
||||
@@ -40,8 +40,6 @@ jobs:
|
||||
# preinstalled: xz, cmake
|
||||
brew install git bison autoconf automake libffi findutils
|
||||
echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH
|
||||
# Mac is using bison 2.5 by default, but we need 3.5+ for swig.
|
||||
echo "$(brew --prefix bison)/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install aws cli
|
||||
run: |
|
||||
@@ -49,11 +47,11 @@ jobs:
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
python ../nix/cache_dependencies.py unpack
|
||||
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: mac-${{ matrix.arch }}
|
||||
|
||||
@@ -83,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: build-logs-osx-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -95,7 +93,9 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
python ../nix/cache_dependencies.py pack
|
||||
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
|
||||
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
|
||||
done
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
|
||||
@@ -26,10 +26,10 @@ jobs:
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/nix/cache_dependencies.py unpack
|
||||
python ../IfcOpenShell/pyodide/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: build-logs-pyodide
|
||||
path: |
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/nix/cache_dependencies.py pack
|
||||
python ../IfcOpenShell/pyodide/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
|
||||
@@ -6,24 +6,18 @@ on:
|
||||
jobs:
|
||||
build_ifcopenshell:
|
||||
runs-on: ubuntu-22.04
|
||||
container: rockylinux:9
|
||||
container: rockylinux:8
|
||||
|
||||
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
|
||||
dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \
|
||||
yum update -y
|
||||
yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
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
|
||||
@@ -44,29 +38,30 @@ jobs:
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
ref: rockylinux9-x64
|
||||
ref: rockylinux8-x64
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
- name: Run Build Script
|
||||
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()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: build-logs-rocky
|
||||
path: |
|
||||
@@ -77,7 +72,9 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
|
||||
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
|
||||
done
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
|
||||
@@ -6,24 +6,18 @@ on:
|
||||
jobs:
|
||||
build_ifcopenshell:
|
||||
runs-on: ubuntu-22.04-arm
|
||||
container: arm64v8/rockylinux:9
|
||||
container: arm64v8/rockylinux:8
|
||||
|
||||
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
|
||||
dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \
|
||||
yum update -y
|
||||
yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
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
|
||||
@@ -44,29 +38,30 @@ jobs:
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
ref: rockylinux9-arm64
|
||||
ref: rockylinux8-arm64
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
- name: Run Build Script
|
||||
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()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: build-logs-rocky-arm64
|
||||
path: |
|
||||
@@ -77,7 +72,9 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
|
||||
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
|
||||
done
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
|
||||
@@ -5,26 +5,11 @@ on:
|
||||
|
||||
jobs:
|
||||
build_ifcopenshell:
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
runs_on: windows-2022
|
||||
deps_dir: _deps-vs2022-x64-installed
|
||||
vcvars: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"'
|
||||
build_branch: windows-x64
|
||||
zip_suffix: win64
|
||||
|
||||
- arch: ARM64
|
||||
runs_on: windows-11-arm
|
||||
deps_dir: _deps-vs2022-ARM64-installed
|
||||
vcvars: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsarm64.bat"'
|
||||
build_branch: windows-arm64
|
||||
zip_suffix: win-arm64
|
||||
|
||||
runs-on: ${{ matrix.runs_on }}
|
||||
|
||||
arch: ['x64']
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -35,8 +20,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ${{ matrix.deps_dir }}
|
||||
ref: ${{ matrix.build_branch }}
|
||||
path: _deps-vs2022-x64-installed
|
||||
ref: windows-${{ matrix.arch }}
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
@@ -46,13 +31,14 @@ jobs:
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd ${{ matrix.deps_dir }}
|
||||
cd _deps-vs2022-x64-installed
|
||||
Get-ChildItem -Path . -Filter 'cache-*.zip' | ForEach-Object {
|
||||
7z x $_.FullName
|
||||
}
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: win-${{ matrix.arch }}
|
||||
# Windows ccache needs ~1GB
|
||||
@@ -61,16 +47,14 @@ jobs:
|
||||
|
||||
- name: Run Build Script And Pack .zip Archives
|
||||
shell: cmd
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.arch }} # lets the Python script know which arch to target (optional override)
|
||||
run: |
|
||||
call ${{ matrix.vcvars }}
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cd win
|
||||
python build-all-win.py
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd ${{ matrix.deps_dir }}
|
||||
cd _deps-vs2022-x64-installed
|
||||
Get-ChildItem -Path . -Directory | ForEach-Object {
|
||||
$cacheFile = "cache-$($_.Name).zip"
|
||||
echo $cacheFile
|
||||
@@ -81,13 +65,12 @@ jobs:
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
cd ${{ matrix.deps_dir }}
|
||||
cd _deps-vs2022-x64-installed
|
||||
git config user.name "IfcOpenBot"
|
||||
git config user.email "ifcopenbot@ifcopenshell.org"
|
||||
git checkout -B ${{ matrix.build_branch }}
|
||||
git add *.zip
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push --set-upstream origin ${{ matrix.build_branch }} || echo "Push failed"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: ci-lint
|
||||
name: ci-black-formatting
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,9 +7,6 @@ on:
|
||||
jobs:
|
||||
lint-formatting:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MIN_IOS_PY_VERSION: "3.10"
|
||||
MIN_BLENDER_PY_VERSION: "3.11"
|
||||
steps:
|
||||
- name: Action - checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -17,12 +14,12 @@ jobs:
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.MIN_IOS_PY_VERSION }}
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -30,17 +27,14 @@ jobs:
|
||||
uv tool install ruff
|
||||
uv tool install black
|
||||
uv tool install poethepoet
|
||||
uv tool install ty==0.0.34
|
||||
|
||||
# black doesn't catch all syntax errors, so we check them explicitly.
|
||||
- name: Check syntax errors
|
||||
id: syntax-errors
|
||||
run: |
|
||||
ERROR=0
|
||||
# Using 2 Python versions - one minimum required for IfcOpenShell
|
||||
# and other that's used by Blender currently.
|
||||
python${{ env.MIN_IOS_PY_VERSION }} -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
||||
python${{ env.MIN_BLENDER_PY_VERSION }} -W error -m compileall -q src/bonsai || ERROR=1
|
||||
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
||||
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
|
||||
exit $ERROR
|
||||
continue-on-error: true
|
||||
|
||||
@@ -58,13 +52,6 @@ jobs:
|
||||
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
|
||||
continue-on-error: true
|
||||
|
||||
- name: ty check
|
||||
id: ty
|
||||
run: |
|
||||
poe ty-venv
|
||||
poe ty
|
||||
continue-on-error: true
|
||||
|
||||
- name: Ruff check
|
||||
id: ruff
|
||||
run: |
|
||||
@@ -95,7 +82,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
|
||||
@@ -112,7 +100,4 @@ jobs:
|
||||
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
|
||||
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
|
||||
fi
|
||||
if [ "${{ steps.ty.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check failed, see 'ty check' step for the details." && ERROR=1
|
||||
fi
|
||||
exit $ERROR
|
||||
@@ -24,15 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
outputs:
|
||||
timestamp: ${{ steps.timestamp.outputs.timestamp }}
|
||||
steps:
|
||||
- name: Get current timestamp
|
||||
id: timestamp
|
||||
# Include hours and minutes to release tag
|
||||
# to avoid possibility of unstable repo's index.json
|
||||
# pointing to the new file when index.json itself wasn't yet updated.
|
||||
run: echo "timestamp=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
@@ -73,6 +67,12 @@ jobs:
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
- name: Get current date
|
||||
id: date
|
||||
# Include hours and minutes to release tag
|
||||
# to avoid possibility of unstable repo's index.json
|
||||
# pointing to the new file when index.json itself wasn't yet updated.
|
||||
run: echo "date=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT
|
||||
- name: Compile
|
||||
run: |
|
||||
cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
|
||||
@@ -88,8 +88,8 @@ jobs:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.find_zip.outputs.filepath }}
|
||||
asset_name: ${{ steps.find_zip.outputs.filename }}
|
||||
release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{ needs.activate.outputs.timestamp }} (unstable)"
|
||||
tag: "bonsai-${{steps.version.outputs.version}}-alpha${{ needs.activate.outputs.timestamp }}"
|
||||
release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}} (unstable)"
|
||||
tag: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}}"
|
||||
overwrite: true
|
||||
body: "See README in https://github.com/IfcOpenShell/bonsai_unstable_repo/ on how to setup autoupdates for daily Bonsai builds."
|
||||
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
|
||||
|
||||
# Download Blender.
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.1/blender-5.1.0-linux-x64.tar.xz
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.0/blender-5.0.1-linux-x64.tar.xz
|
||||
tar -xf blender.tar.xz
|
||||
|
||||
# Setup Blender.
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
pip install -r requirements.txt
|
||||
python setup_extensions_repo.py --last-tag
|
||||
cd ..
|
||||
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py313*-linux-x64.zip)"
|
||||
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py311*-linux-x64.zip)"
|
||||
|
||||
# Install Bonsai.
|
||||
blender --command extension install-file -r user_default -e $bonsai_zip
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py311, py312, py313]
|
||||
pyver: [py311, py312]
|
||||
config:
|
||||
- {
|
||||
name: "Windows Build",
|
||||
@@ -42,11 +42,6 @@ jobs:
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1,
|
||||
}
|
||||
exclude:
|
||||
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
|
||||
- pyver: py313
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: ci-ifcedit-pypi
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifcedit &&
|
||||
make dist IS_STABLE=TRUE
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifcedit/dist
|
||||
@@ -1,36 +0,0 @@
|
||||
name: ci-ifcmcp-pypi
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifcmcp &&
|
||||
make dist IS_STABLE=TRUE
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifcmcp/dist
|
||||
verbose: true
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
environment-name: test-env
|
||||
create-args: >-
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
run: |
|
||||
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
|
||||
|
||||
- uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
environment-name: test-env
|
||||
create-args: >-
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
-
|
||||
name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
-
|
||||
name: Build ifcopenshell
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
make package
|
||||
working-directory: build
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
@@ -91,26 +91,26 @@ jobs:
|
||||
lfs: true
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v8.0.1
|
||||
uses: actions/download-artifact@v7.0.0
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
path: artifacts/
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to Dockerhub
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: aecgeeks
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
-
|
||||
name: Build container image
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: artifacts
|
||||
repository: aecgeeks/ifcopenshell
|
||||
|
||||
@@ -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,10 @@ jobs:
|
||||
libhdf5-dev libcgal-dev libeigen3-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
# TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released
|
||||
# to update hardcoded references to commits in some other workflows.
|
||||
# Then we can switch back to 1.2 in all actions.
|
||||
uses: hendrikmuhs/ccache-action@v1.2.19
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
@@ -185,7 +184,6 @@ jobs:
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DWITH_ROCKSDB=On \
|
||||
-DBUILD_EXAMPLES=ON \
|
||||
../cmake
|
||||
sudo make -j $(nproc)
|
||||
sudo make install
|
||||
@@ -256,26 +254,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
|
||||
|
||||
+7
-17
@@ -5,8 +5,6 @@
|
||||
/_installed-vs*-x*/
|
||||
/build/
|
||||
/src/examples/build/
|
||||
# ifctester docs output
|
||||
/src/ifctester/test/build/
|
||||
|
||||
# output directories
|
||||
/cmake/out/
|
||||
@@ -14,7 +12,6 @@
|
||||
/src/ifcmax/out/
|
||||
/src/ifcwrap/out/
|
||||
/src/qtviewer/out/
|
||||
/src/ifctester/webapp/public/pyodide/
|
||||
|
||||
/win/BuildDepsCache*.txt
|
||||
|
||||
@@ -28,7 +25,6 @@ venv
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
.vs
|
||||
/*.code-workspace
|
||||
|
||||
# PyCharm files
|
||||
.idea
|
||||
@@ -84,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/
|
||||
@@ -124,9 +116,7 @@ dev_environment.bat
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||
|
||||
|
||||
# temp files from AI coding tools
|
||||
*.claude
|
||||
*.py.tmp*
|
||||
*.json.tmp*
|
||||
|
||||
# Claude Code local config (managed via dotfiles repo)
|
||||
CLAUDE.md
|
||||
CLAUDE.local.md
|
||||
.mcp.json
|
||||
|
||||
@@ -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}")
|
||||
|
||||
+17
-35
@@ -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)
|
||||
|
||||
@@ -81,7 +80,7 @@ option(BUILD_IFCGEOM "Build IfcGeom." ON)
|
||||
option(BUILD_IFCPYTHON "Build IfcPython." ON)
|
||||
option(BUILD_CONVERT "Build IfcConvert executable." ON)
|
||||
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
|
||||
option(BUILD_EXAMPLES "Build example applications." OFF)
|
||||
option(BUILD_EXAMPLES "Build example applications." ON)
|
||||
option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." ON)
|
||||
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
|
||||
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
|
||||
@@ -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)
|
||||
# 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 $<IF:$<TARGET_EXISTS:RocksDB::rocksdb-shared>,RocksDB::rocksdb-shared,RocksDB::rocksdb>
|
||||
)
|
||||
|
||||
if(WITH_ZSTD)
|
||||
# @todo do we actually need the zstd include dir or rather just pass
|
||||
@@ -373,20 +368,12 @@ if(ENABLE_BUILD_OPTIMIZATIONS)
|
||||
|
||||
# Linker
|
||||
# /OPT:REF enables also /OPT:ICF and disables INCREMENTAL
|
||||
set(LINKER_FLAGS_RELEASE "/LTCG /OPT:REF")
|
||||
# /OPT:NOICF is recommended when /DEBUG is used (http://msdn.microsoft.com/en-us/library/xe4t6fc1.aspx)
|
||||
set(LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /OPT:NOICF")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF")
|
||||
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO
|
||||
"${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}"
|
||||
)
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}")
|
||||
set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO
|
||||
"${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}"
|
||||
)
|
||||
# /OPT:NOICF is recommended when /DEBUG is used (http://msdn.microsoft.com/en-us/library/xe4t6fc1.aspx)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF")
|
||||
else()
|
||||
# GCC-like: Release should use O3 but RelWithDebInfo 02 so enforce 03. Anything other useful that could be added here?
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
||||
@@ -661,11 +648,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)
|
||||
@@ -43,17 +43,6 @@ if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
|
||||
set_target_properties(TKernel PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${OpenCASCADE_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
if(
|
||||
OpenCASCADE_VERSION VERSION_LESS "7.9.0"
|
||||
AND CMAKE_VERSION GREATER_EQUAL "3.24"
|
||||
AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU"
|
||||
)
|
||||
# Before 7.9.0 targets in OCCT cmake configs are not linked to each other
|
||||
# leading to missing symbols on Unix. Link them as a single group as a workaround.
|
||||
# Only needed for gcc, because other compilers (e.g. Apple Clang, MSVC) do rescan automatically.
|
||||
set(OpenCASCADE_LIBRARIES "$<LINK_GROUP:RESCAN,${OpenCASCADE_LIBRARIES}>")
|
||||
endif()
|
||||
|
||||
if(OpenCASCADE_VERSION VERSION_LESS "7.9.0" AND WIN32)
|
||||
# Bug in OCCT cmake configs < 7.9.0 - missing linked library.
|
||||
list(APPEND OpenCASCADE_LIBRARIES WSOCK32.lib)
|
||||
|
||||
@@ -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,400 +0,0 @@
|
||||
<!-- This file was generated with the assistance of an AI coding tool. -->
|
||||
|
||||
# Linked file features — queries, styles, transforms, and multi-linking for linked IFC models
|
||||
|
||||
> **Living dev note** for the `Linked_File_Features` branch/PR. Read before working
|
||||
> on the feature; append decisions and findings as the PR is refined. This is *not* user
|
||||
> documentation — at merge it is removed or its durable parts promoted to code comments.
|
||||
> See [README.md](README.md) for the convention (introduced on the
|
||||
> `opening-template-on-type` branch; not yet on this branch's base).
|
||||
|
||||
## Problem
|
||||
|
||||
Linked IFC models (`bim.link_ifc`) had several gaps that made them hard to use as a
|
||||
"reference in other trades' models" workflow:
|
||||
|
||||
- One shared `.ifc.cache.blend` per IFC file meant the **same file could not be linked
|
||||
twice with different selector queries** — both links showed whichever query was cached
|
||||
first in-session, and whichever was cached last after reopening (Blender reuses one
|
||||
library datablock per path).
|
||||
- The selector query was not durably stored anywhere in the host IFC, so save → reopen
|
||||
lost or cross-wired the filter; a scripted `bpy.ops.bim.reload_link()` also wiped it.
|
||||
- Linked geometry got **flat diffuse-only materials** — external `.blend` styles
|
||||
(`IfcExternallyDefinedSurfaceStyle`) and per-layer materials (layerset slicing) that
|
||||
the normal import applies were ignored.
|
||||
- Moving a linked model required an explicit enable-edit → move → save dance on the
|
||||
active link only, with save/cancel buttons in the panel header.
|
||||
- The Explore tool's highlight broke (GPU type errors), drew at the link's *original*
|
||||
location when the link had been moved, and `bim.append_inspected_linked_element`
|
||||
placed appended elements at the original location too.
|
||||
|
||||
## Key facts established
|
||||
|
||||
- **Cache architecture**: `LoadLink.link_ifc` generates a Python script and runs a
|
||||
background Blender subprocess that executes `bim.load_linked_project` and saves a
|
||||
`.ifc.cache.blend`. The host session then *links* (not appends) the `IfcProject/...`
|
||||
collection from that blend and instances it via an empty (the link "handle").
|
||||
Georeferencing metadata lives in a sidecar `.cache.json`; extracted properties in
|
||||
`.cache.sqlite` (whole file, query-independent — deliberately shared across queries).
|
||||
- **Blender reuses an in-session library per path.** Loading the same blend path twice
|
||||
yields the same library/collection. This is what broke multi-query linking with a
|
||||
shared cache filename, and why per-query *filenames* (not cache invalidation) are the
|
||||
fix.
|
||||
- **Last-used operator properties** are reused on the next *interactive* invocation
|
||||
(UI button), while scripted `bpy.ops` calls always start from defaults. LoadLink's
|
||||
internal `self.query = link.query` fallback assignment was remembered by Blender and
|
||||
leaked into the next button click (`operator_query='IfcWindow'` for the door link).
|
||||
Any `is_property_set()`-based logic is corrupted the same way. Fix: `SKIP_SAVE` on
|
||||
volatile props. **A GUI-only bug like this is invisible to scripted repro** — both
|
||||
headless and windowed `--python` test runs passed while the manual flow failed.
|
||||
- **`IfcDocumentReference`** per link: attribute index 1 (`Identification`) already
|
||||
stores the link's 4×4 transformation (existing Bonsai convention). `Description`
|
||||
(IFC4+; **absent in IFC2X3**) now stores the selector query. One
|
||||
`IfcDocumentInformation` (Scope `LINKED_MODEL`) per file, one reference per link.
|
||||
- **Geometry iterator materials**: `material.instance_id()` is the STEP id of the
|
||||
`IfcSurfaceStyle` — or of an `IfcMaterial` when the item has a material but no style,
|
||||
hence the `is_a("IfcSurfaceStyle")` guard when resolving external styles.
|
||||
- **External styles**: `IfcExternallyDefinedSurfaceStyle.Location` (`.blend`, relative
|
||||
paths resolve against the *linked* IFC, not the host) + `Identification` in
|
||||
`data_block_type/name` form (e.g. `materials/Brick`), same convention as
|
||||
`bim.activate_external_style`.
|
||||
- **Chunk pipeline dedups materials by RGBA color** (`np.unique` on a color array), so
|
||||
style identity must ride along as an extra column to survive — added only for styles
|
||||
that actually resolve to an external material, so plain colored styles dedupe exactly
|
||||
as before.
|
||||
- **`slice_layerset_mesh` needs a local-space, per-element mesh** (bisect planes are in
|
||||
object space), which the chunk path can't provide (world-space, many elements per
|
||||
mesh) — hence routing multi-layer elements through the instanced path. Its
|
||||
`dissolve_limit` produces **ngons**, which broke the Explore highlight's
|
||||
triangles-from-`polygon.vertices` assumption downstream.
|
||||
- **ID properties round-trip as `IDPropertyArray`**, not plain lists (verified in
|
||||
4.5.7: empty list → flat `IDPropertyArray`; nested lists → list of `IDPropertyArray`
|
||||
items), and `GPUIndexBuf` rejects them — selection geometry must be converted to
|
||||
plain tuples on read.
|
||||
- **`scene.ray_cast` returns the hit instance's world matrix** (link empty matrix
|
||||
included). For instanced occurrence objects the object's own local matrix is *not*
|
||||
identity, so resolving the instancing empty must compare against
|
||||
`empty.matrix_world @ obj.matrix_world`, not the empty's matrix alone.
|
||||
- **Link matrix math**: the handle empty's matrix is `inv(L) @ T @ G` (L = host local
|
||||
matrix from georef props, T = stored transformation, G = linked model's global
|
||||
matrix from the cache json). The world-space displacement of a moved link is
|
||||
therefore `inv(L) @ T @ L` — no json read needed (`calculate_link_delta_matrix`).
|
||||
- **Undo consistency of auto-saved moves**: Blender undo of a handle move fires another
|
||||
depsgraph update, so the handler re-saves the reverted matrix — stored state stays
|
||||
consistent without transactions (a handler can't open one).
|
||||
|
||||
## Design
|
||||
|
||||
### Per-query caches + query persistence (multi-linking)
|
||||
|
||||
`tool.Project.get_link_cache_paths(filepath, query)` appends `.md5(query)[:8]` to the
|
||||
cache blend/json names; the empty query keeps the legacy un-suffixed names so existing
|
||||
caches stay valid. Every cache-path consumer goes through it — `link_ifc` build and
|
||||
invalidation, the subprocess json write, model-origin/georef indicator reads,
|
||||
`calculate_link_matrix`, `save_link_transformation`, and the per-link
|
||||
selectability/wireframe/visibility toggles (which match collections *by library
|
||||
filepath* and would otherwise affect every link of the file at once).
|
||||
|
||||
The query persists on each link's `IfcDocumentReference.Description` (written by
|
||||
`LinkIfc` and `ReloadLink`); `load_linked_models_from_ifc` restores from it, with a
|
||||
legacy-JSON fallback that only applies when the file has a **single** link (with
|
||||
several links the shared JSON can't say which link it belonged to). IFC2X3 hosts have
|
||||
no `Description` — custom queries are not restorable there (accepted).
|
||||
|
||||
`LoadLink`/`ReloadLink` volatile properties are `SKIP_SAVE` (see key facts). Cache
|
||||
clearing tolerates a missing blend (a reload with a brand-new query points at a
|
||||
not-yet-existing filename).
|
||||
|
||||
### Include/Exclude filter pair
|
||||
|
||||
The selector grammar's only cross-group combiner is `+` (union) and the `parent`
|
||||
facet cannot express "not under X" (its `!=`/regex paths also match by GlobalId, so
|
||||
negation removes everything with any parent), which makes set differences like
|
||||
"group members minus the slabs under aggregate X" structurally inexpressible in one
|
||||
query string. Links therefore carry an **Exclude** query beside the include —
|
||||
mirroring `EPset_Drawing`'s Include/Exclude pattern: final set = include (or the
|
||||
default set when empty) − exclude, applied in `LoadLinkedProject` and per link in
|
||||
`create_drawing`.
|
||||
|
||||
- **Cache key**: `get_link_cache_paths` hashes `md5(query + "\0" + exclude)` when an
|
||||
exclude exists; include-only filters keep the pre-exclude `md5(query)` so existing
|
||||
caches stay valid; empty filter keeps legacy un-suffixed names. Keying on query
|
||||
alone would let same-include/different-exclude links silently serve each other's
|
||||
geometry.
|
||||
- **Persistence**: `encode_link_filter`/`decode_link_filter` — a plain include is
|
||||
stored in `Description` as-is (backwards compatible); an exclude, a `loaded`
|
||||
state or a custom display name promotes the value to
|
||||
`{"include": …, "exclude": …, "loaded": …, "name": …}` JSON. Decode treats
|
||||
non-JSON as a legacy include string. The display name (`Link.display_name`,
|
||||
double-click the list row to rename; file path shows as placeholder while
|
||||
unset) exists to tell apart several links of the same file.
|
||||
- Exclude applies on top of the **default** element set too, so
|
||||
"everything except X" needs no explicit include.
|
||||
- UI labels are **Include**/**Exclude** (matching the drawing pattern), but the
|
||||
property identifier stays `query` for script (`bpy.ops.bim.link_ifc(query=…)`)
|
||||
and persistence compatibility.
|
||||
- Verified headless: `query=""`/`exclude="IfcDoor"` loads only the window;
|
||||
same file with a different filter gets its own cache; both filters survive
|
||||
save → reopen → reload.
|
||||
|
||||
### Auto-load on open
|
||||
|
||||
Links that were **loaded and visible** at IFC save time auto-load when the project
|
||||
is reopened. `ExportIFC` calls `tool.Project.update_linked_models_state()`, which
|
||||
rewrites each reference's `Description` with a `loaded` flag
|
||||
(`is_loaded and not is_hidden`); `load_linked_models_from_ifc` replays flagged
|
||||
links via `load_link` after restoring the list (missing files warn and skip so
|
||||
they can't break project open). The flag extends the same JSON blob as the
|
||||
exclude — plain legacy strings decode as no-autoload. Trade-off: project open
|
||||
pays the link-load cost up front (fast on cache hit; a missing cache rebuilds in
|
||||
a background Blender, same as clicking Load). Verified headless: loaded+visible
|
||||
auto-loads; unloaded and loaded-but-hidden links stay unloaded.
|
||||
|
||||
### Long-term serialization target: STEP Part 21 Edition 3
|
||||
|
||||
STEP p21e3 defines the standards-track version of this feature's persistence:
|
||||
`ANCHOR`/`REFERENCE` sections (clauses 9–10) let one file import entities from
|
||||
another via URI + fragment, and **anchor tags** (`{tagname: value}`) are the
|
||||
designated slot for out-of-schema metadata — a cleaner home than the
|
||||
`Description` JSON blob (see the review-round discussion). ifcopenshell does not
|
||||
implement these sections yet ([#668](https://github.com/IfcOpenShell/IfcOpenShell/issues/668),
|
||||
open, unassigned); if it ever does, the migration path is: link →
|
||||
`REFERENCE` to the linked file's project anchor, filter/transform/loaded
|
||||
metadata → anchor tags. Keeping the blob behind
|
||||
`encode_link_filter`/`decode_link_filter` makes that a two-function change.
|
||||
|
||||
Two p21e3 design points this branch already conforms to:
|
||||
|
||||
- **Identity**: p21e3 distinguishes volatile file-scoped entity numbers
|
||||
(`#100` fragments) from durable anchors/UUIDs — the same lesson behind our
|
||||
STEP-id collision fixes (GUID-based matching, `element.file` guards). Raw
|
||||
STEP ids must never cross a file boundary; IFC GlobalIds map 1:1 onto
|
||||
p21e3 UUID anchors.
|
||||
- **Transport** (clause A.4): exchange structures plus referenced resources
|
||||
can ship as one ZIP archive with references resolving inside it. Our posix,
|
||||
optionally relative `Location`s resolved via `resolve_uri` are exactly the
|
||||
invariants a future "package project with links" export would need.
|
||||
|
||||
Even full p21e3 support would not cover per-link transforms, filters, or load
|
||||
state — a `REFERENCE` imports entities, it does not place a model — so the
|
||||
app-level metadata remains; only its container would change.
|
||||
|
||||
### External styles + layerset slicing in the linked loader
|
||||
|
||||
`LoadLinkedProject.get_external_material(style_id)` resolves a style id → appended
|
||||
Blender material from the external `.blend`, cached two ways (per style id; per
|
||||
appended data-block, so styles sharing one material don't append duplicates). Appended
|
||||
materials get their stale `ifc_definition_id` cleared (the source `.blend` may have
|
||||
been authored in a Bonsai session; the id would be misread in the linked file *and*
|
||||
in the host once the cache links in). Applied in both loading paths — instanced
|
||||
occurrences directly, chunks via the style-id column.
|
||||
|
||||
Multi-layer elements (`IfcMaterialLayerSetUsage`, >1 layer) route through the
|
||||
instanced path and get `slice_layerset_mesh`, which gained a pluggable
|
||||
`style_to_material` resolver (defaults to the old `tool.Ifc.get_object` for the normal
|
||||
import) — the linked resolver prefers the external material, falling back to a flat
|
||||
diffuse from the style's shading colour. Also fixed there: newly appended layer
|
||||
materials are registered in the dedup dict (two layers sharing one style used to
|
||||
append it twice).
|
||||
|
||||
Trade-off: layered walls become individual instanced objects instead of chunk members;
|
||||
meshes shared between elements (same geometry id) bake the slice from the first
|
||||
element's layerset usage — same behaviour as the normal importer.
|
||||
|
||||
### Reload Link dialog
|
||||
|
||||
`bim.reload_link` now exposes File Path (+ browse button), Use Relative Path
|
||||
(defaulting to the stored path form), Use Cache (default off = old always-rebuild
|
||||
behaviour), the False Origin Mode project props, and Query. A file browser can't open
|
||||
from inside a props dialog, so the browse button runs `bim.select_link_filepath`
|
||||
(fileselect) which *reopens* the reload dialog with the chosen path, carrying the
|
||||
in-progress dialog state through the round trip (op props are baked at draw time).
|
||||
Path changes update `link.name`/`filepath` and, with a host IFC, the reference
|
||||
`Location` + document name — which is why `ReloadLink` became a `tool.Ifc.Operator`.
|
||||
Script calls without arguments preserve all stored link values via `is_property_set`.
|
||||
|
||||
`bim.reload_all_links` (refresh button beside Link IFC in the panel header) reloads
|
||||
every *loaded* link via argument-less `reload_link` calls — each link's stored
|
||||
path/query/exclude replay and its cache rebuilds from disk. Unloaded links are left
|
||||
alone. Deliberately expensive: one background cache rebuild per link.
|
||||
|
||||
### Per-row lock toggle + auto-saved transforms
|
||||
|
||||
Link editing moved from the panel header into each list row as a lock/unlock icon:
|
||||
unlock (`bim.enable_editing_link`) frees the handle; **any movement is persisted
|
||||
immediately** by a `depsgraph_update_post` handler (lazy — ticks without transform
|
||||
updates cost ~nothing); lock (`bim.disable_editing_link`) saves and locks.
|
||||
`bim.edit_link` and the explicit save step are **removed**; cancel/restore semantics
|
||||
no longer exist (undo or move it back). The save math lives in
|
||||
`tool.Project.save_link_transformation`. Enable/disable take a `link_index`
|
||||
(default −1 = active link) so several links can be edited at once and script calls
|
||||
stay compatible.
|
||||
|
||||
### Explore tool + append fixes for moved links
|
||||
|
||||
- Highlight triangles come from `mesh.calc_loop_triangles()` filtered to the queried
|
||||
element's polygon range (ngon-safe); edges keep `polygon.edge_keys` (no diagonals).
|
||||
- `get_selected_geometry` converts the ID-prop round trip to plain tuples (GPU
|
||||
rejects `IDPropertyArray`); TRIS drawing gated on its own data.
|
||||
- `QueryLinkedElement` passes the ray-cast instance matrix through;
|
||||
`find_obj_root` compares it against `empty @ obj_local` and falls back to the
|
||||
collection's only instance when no matrix is available (select-by-GUID flow).
|
||||
- `bim.append_inspected_linked_element` pre-multiplies the imported object's matrix by
|
||||
`calculate_link_delta_matrix(link)`, matching the link by the queried instance's
|
||||
root empty first (filepath alone is ambiguous with several links per file). The
|
||||
element's IFC placement syncs to the moved location on save — intended.
|
||||
|
||||
### Drawings (`create_drawing`) — moved links and per-link queries
|
||||
|
||||
- The linework serializer opened linked IFCs raw, so a moved link's elements were
|
||||
drawn at their *original* coordinates (usually outside the drawing extents —
|
||||
"linked objects disappear from prints after moving the link").
|
||||
- The stored link transformation is already the **model-space** delta (that is how
|
||||
`save_link_transformation` derives it), which is exactly the space the serializer
|
||||
works in — so it can be baked straight into the geometry iterator via the existing
|
||||
`model-offset`/`model-rotation` settings. The mapping composes
|
||||
`Trans(model-offset) @ Rot(model-rotation)` (see `mapping.cpp`), matching the
|
||||
`Trans(t) @ Rot(R)` decomposition of the rigid link matrix; `model-rotation` is a
|
||||
quaternion passed as `(x, y, z, w)`. The pre-existing 2mm plan-view Z-offset simply
|
||||
adds onto the translation (translations commute).
|
||||
- The serialization loop previously collected files in a dict keyed by filepath, which
|
||||
**collapsed same-file links into one pass** (one transform — the last link's — and
|
||||
no query awareness): with two links of one file, only one showed in the drawing.
|
||||
It now iterates one entry per link (`(path, file, transform, query)` tuples), and
|
||||
intersects each link's drawing elements with
|
||||
`ifcopenshell.util.selector.filter_elements(ifc, link.query)` so the drawing shows
|
||||
what that link actually displays in the viewport.
|
||||
- `tool.Project.get_link_transformation_matrix(link)` is the shared accessor for the
|
||||
stored 4×4 (None when identity/absent).
|
||||
- Verified headless with the window/door kit: moved window offset in the SVG by
|
||||
exactly 5m × scale; unmoved door at its native position; both links present.
|
||||
|
||||
### Drawings — `.cut` styling for linked models (BISECT cut mode)
|
||||
|
||||
- The default **BISECT** cut mode deletes the OpenCASCADE serializer's cut linework
|
||||
(`remove_cut_linework`) and regenerates cuts by bisecting **Blender mesh objects**
|
||||
(`generate_bisect_linework` over `context.visible_objects`). Linked models are
|
||||
instanced collections with no mesh objects, so their cuts were deleted and never
|
||||
regenerated — linked elements only ever appeared as `projection`, and the `.cut`
|
||||
CSS rule never applied to them. Long-standing gap, unrelated to moved links
|
||||
(A/B-tested against pre-branch code: identical).
|
||||
- Fix: `remove_cut_linework` only removes cut groups whose guid resolves in the
|
||||
**host** file — linked elements keep the serializer's cut geometry, which the
|
||||
merge step then classes as `cut`.
|
||||
- **Cross-file STEP-id collision**: `tool.Ifc.get_object(linked_entity)` resolves the
|
||||
entity's STEP id against the *host* session's id map and can return an arbitrary
|
||||
host object (in the test project: the drawing camera, crashing
|
||||
`generate_material_layers` with "expected 'Mesh' found 'Camera'"). Guarded via
|
||||
`element.file is tool.Ifc.get()` in `generate_material_layers` and the merge step.
|
||||
- **Paint order**: the projection-under-cut convention was enforced only in
|
||||
OPENCASCADE mode (`move_projection_to_bottom`); BISECT appends its own cut paths
|
||||
last so it never needed it — but the retained serializer cuts of linked models are
|
||||
emitted *before* the projections. BISECT now runs the same pass; `BringToFront`
|
||||
(`move_elements_to_top`) still gets the final say.
|
||||
- Known limitation: linked cut paths are raw serializer output — they skip the
|
||||
shapely path-closing/merging and the material-layer hatching pass (both need host
|
||||
Blender objects). Stroke + fill from `.cut` CSS apply; layered hatching inside
|
||||
linked cuts is a candidate follow-up.
|
||||
- Debugging note: merged cut groups carry member guids as CSS *classes*, not as the
|
||||
`ifcopenshell:guid` attribute — inspect both when checking cut output.
|
||||
|
||||
## Deferred refactors (deliberate)
|
||||
|
||||
- **Upstream `exclude=` on `filter_elements`** — the include−exclude set difference
|
||||
is hand-rolled twice (links, drawings) because the selector grammar has no
|
||||
difference operator and `parent` negation is broken by design (its `!=`/regex
|
||||
paths also match GlobalIds, so negation strips everything that has a parent).
|
||||
The right home is an `exclude=` parameter on
|
||||
`ifcopenshell.util.selector.filter_elements`, documented in
|
||||
`selector_syntax.rst` together with the `parent`-negation limitation. Deferred
|
||||
to a separate ifcopenshell-python PR (different review audience; would widen
|
||||
this PR mid-review). Once it lands, both Bonsai call sites collapse.
|
||||
- **Core/tool ceremony skipped** — the new `tool.Project` methods have no
|
||||
`core/tool.py` interface declarations and no `bonsai/core` orchestration
|
||||
functions, matching the pre-existing linked-model code (which bypasses the
|
||||
core layer wholesale; `LoadLinkedProject` is flagged "prototyping" upstream).
|
||||
Interfaces nobody calls through wouldn't add testability — the pure helpers
|
||||
(`encode_link_filter`/`decode_link_filter`, `get_link_cache_paths`) are
|
||||
covered directly in `test/tool/test_project.py` instead. Revisit if the
|
||||
linked-model subsystem is ever promoted out of prototype status.
|
||||
|
||||
## Review round 1 (PR #8242, falken10vdl) — decisions
|
||||
|
||||
- **Path-form mismatch → duplicate documents (confirmed bug, fixed).**
|
||||
`get_linked_models_documents()` keyed documents by the *stored* `Location`, so
|
||||
linking the same file first relative then absolute (or vice versa) created a second
|
||||
`IfcDocumentInformation`. Both sides of the lookup now normalize through
|
||||
`tool.Ifc.resolve_uri()` before matching.
|
||||
- **`Description` for the query — kept.** It is implementation metadata in an IFC
|
||||
attribute, but consistent with the existing convention on these same references
|
||||
(`Identification` stores the 4×4 transformation, a bigger stretch). References are
|
||||
Bonsai-managed (`Scope="LINKED_MODEL"`), so user-description collisions are unlikely.
|
||||
A cleaner consolidated convention (query + transform + options in one serialized
|
||||
attribute) is a candidate follow-up, deliberately out of scope here.
|
||||
- **`md5(query)[:8]` — kept.** 32 bits ≈ birthday collision at ~65k distinct queries
|
||||
*per file*; and a collision is not silent: the cache JSON stores the full query and
|
||||
`should_clear_cache()` compares it, so a colliding cache is detected and rebuilt
|
||||
(self-healing).
|
||||
- **Depsgraph autosave vs save-on-lock — autosave kept.** Save-on-lock alone loses the
|
||||
"what you see is what's saved" guarantee (move + save project without locking =
|
||||
silently dropped move) and loses undo tracking (undo fires a depsgraph update that
|
||||
re-saves the reverted transform). The handler early-outs when no links exist and only
|
||||
works on ticks containing an object-transform update while a link is unlocked.
|
||||
|
||||
## Status — implemented (verified in Blender, incl. headless + GUI repro runs)
|
||||
|
||||
Six commits on `Linked_File_Features`:
|
||||
|
||||
- `0096c0f6a2` reload_link without a query preserves the stored one.
|
||||
- `40db55e52d` external styles + layerset slicing for linked models
|
||||
(`project/operator.py`, `tool/loader.py`).
|
||||
- `d210d4c814` full Reload Link dialog + `bim.select_link_filepath`.
|
||||
- `3dc161f0f2` per-row lock toggle, auto-save handler, `edit_link` removed
|
||||
(`project/operator.py`, `project/ui.py`, `project/__init__.py`, `tool/project.py`).
|
||||
- `0571d22855` Explore highlight (ngons, IDPropertyArray), moved-link highlight,
|
||||
append placement (`tool/project.py`, `project/operator.py`, `project/decorator.py`).
|
||||
- `c14592ec0a` per-query caches, Description persistence, SKIP_SAVE.
|
||||
|
||||
Plus:
|
||||
|
||||
- `ee43ed5526` review-round path normalization in `get_linked_models_documents` /
|
||||
`LinkIfc` (see Review round 1).
|
||||
- `1669cbcd43` drawing support for moved links and per-link queries in
|
||||
`create_drawing` (`drawing/operator.py`, `tool/project.py`).
|
||||
- `.cut` styling for linked models in BISECT cut mode + STEP-id collision guards +
|
||||
paint order (`drawing/operator.py`) — committed together with this note update.
|
||||
|
||||
End-to-end verified with a two-links-one-file kit (window/door, distinct queries):
|
||||
correct visuals on load, after save → reopen → reload, in both headless and windowed
|
||||
Blender.
|
||||
|
||||
## Things to test / verify
|
||||
|
||||
- **IFC2X3 host**: `Description` doesn't exist — link queries silently not restored on
|
||||
reopen (legacy fallback only for single-link files). Acceptable? Warn?
|
||||
- **Relative-path links** (`use_relative_path`) through the whole cycle: cache paths,
|
||||
reference `Location`, reload path change, query restore. The duplicate-document case
|
||||
(same file linked relative then absolute) is fixed — verify one document with two
|
||||
references via `IfcDocumentInformation.HasDocumentReferences`.
|
||||
- Same file linked twice, **both moved differently**: Explore highlight and append
|
||||
placement per instance (root-empty matching), per-link visibility toggles.
|
||||
- External styles with **image textures**: paths relative to the style's source
|
||||
`.blend` may not resolve from the cache blend's location (shared limitation with the
|
||||
normal import path).
|
||||
- Stale cache orphans: per-query filenames accumulate one blend+json pair per distinct
|
||||
query next to the IFC; nothing auto-deletes them. Cleanup on unlink? Document?
|
||||
- Mid-drag auto-save writes the IFC reference outside Bonsai's transaction system —
|
||||
confirm no undo-stack weirdness in longer editing sessions.
|
||||
- Layerset slicing on meshes shared by elements with *different* usages (offset/sense)
|
||||
bakes the first element's slice — same as normal import, but worth a look with types.
|
||||
- `bim.select_link_filepath` round trip when the reload dialog was opened for a
|
||||
non-active link, and dialog-state carry-over after editing the query *then* browsing.
|
||||
- **Drawing SVG guid cache vs moved links**: `create_drawing` skips elements whose
|
||||
guids already exist in the drawing's SVG (`cached_linework`, invalidated only for
|
||||
*edited host objects*). Moving a link does not invalidate its elements, so a
|
||||
regenerated drawing keeps their old positions until the SVG is deleted. Candidate
|
||||
fix: subtract a moved link's guids from `cached_linework` (compare stored transform
|
||||
against the one recorded at last generation).
|
||||
- Same element appearing in two links of one file (overlapping queries) serializes
|
||||
twice with different transforms; the SVG guid cache keeps whichever came first on
|
||||
regeneration. Degenerate case — probably fine to ignore, but note it.
|
||||
+83
-70
@@ -1,6 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
# /// script
|
||||
# ///
|
||||
###############################################################################
|
||||
# #
|
||||
# This file is part of IfcOpenShell. #
|
||||
@@ -77,30 +75,27 @@ Used environment variables:
|
||||
# #
|
||||
# for python37 to install correctly additionally: #
|
||||
# * libffi(-dev[el]) #
|
||||
# for Python build we also needs ssl and zlib #
|
||||
# for Python build we also needs ssl #
|
||||
# (since we do `pip install numpy` at the end) #
|
||||
# * libssl-dev #
|
||||
# #
|
||||
# on debian 7.8 these can be obtained with: #
|
||||
# $ apt-get install git gcc g++ autoconf bison bzip2 cmake #
|
||||
# mesa-common-dev libffi-dev libfontconfig1-dev #
|
||||
# libssl-dev xz zlib1g-dev #
|
||||
# libssl-dev xz #
|
||||
# #
|
||||
# on ubuntu 14.04: #
|
||||
# $ apt-get install git gcc g++ autoconf bison make cmake #
|
||||
# mesa-common-dev libffi-dev libfontconfig1-dev #
|
||||
# libssl-dev xz-utils zlib1g-dev #
|
||||
# libssl-dev xz-utils #
|
||||
# #
|
||||
# on OS X El Capitan with homebrew: #
|
||||
# $ brew install git bison autoconf automake libffi cmake #
|
||||
# $ # `bison` shipped with Mac is too old for swig build, #
|
||||
# $ # so we use `brew`. #
|
||||
# $ export PATH=$(brew --prefix bison)/bin:$PATH #
|
||||
# #
|
||||
# on RHEL-related distros: #
|
||||
# $ dnf install git gcc gcc-c++ autoconf bison make cmake #
|
||||
# $ yum install git gcc gcc-c++ autoconf bison make cmake #
|
||||
# mesa-libGL-devel libffi-devel fontconfig-devel bzip2 #
|
||||
# automake patch byacc xz zlib-devel openssl-devel #
|
||||
# automake patch byacc xz #
|
||||
|
||||
"""
|
||||
|
||||
@@ -126,9 +121,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()
|
||||
@@ -145,8 +147,9 @@ OCCT_VERSION = "7.8.1"
|
||||
BOOST_VERSION = "1.86.0"
|
||||
EIGEN_VERSION = "3.4.0"
|
||||
PCRE_VERSION = "8.41"
|
||||
PCRE2_VERSION = "10.32"
|
||||
LIBXML2_VERSION = "2.13.8"
|
||||
SWIG_VERSION = "4.2.1"
|
||||
SWIG_VERSION = "4.1.0"
|
||||
OPENCOLLADA_VERSION = "v1.6.68"
|
||||
HDF5_VERSION = "1.13.1"
|
||||
|
||||
@@ -243,8 +246,15 @@ if WASM:
|
||||
# https://github.com/pyodide/pyodide-build/pull/249
|
||||
WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (99, 0, 0)
|
||||
|
||||
# 0.31 is required for SIDE_MODULE_CXXFLAGS to be provided.
|
||||
assert get_pyodide_build_version() >= (0, 31)
|
||||
# pyodide provide empty `CXXFLAGS`, leading to issues using C++ files compiled with `-fexceptions`
|
||||
# which is used by OCCT.
|
||||
# https://github.com/pyodide/pyodide-build/issues/251
|
||||
side_module_cxx_flags = os.environ.get("SIDE_MODULE_CXXFLAGS", "")
|
||||
if side_module_cxx_flags.strip():
|
||||
print("SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').")
|
||||
print("Maybe it's time to stop overriding them in the script?")
|
||||
|
||||
os.environ["SIDE_MODULE_CXXFLAGS"] = os.environ["SIDE_MODULE_CFLAGS"]
|
||||
|
||||
# Set defaults for missing empty environment variables
|
||||
|
||||
@@ -284,7 +294,6 @@ DEPS_DIR = os.getenv("DEPS_DIR", DEFAULT_DEPS_DIR)
|
||||
if not os.path.exists(DEPS_DIR):
|
||||
os.makedirs(DEPS_DIR)
|
||||
|
||||
INSTALL_DIR = Path(DEPS_DIR) / "install"
|
||||
BUILD_CFG = os.getenv("BUILD_CFG", "RelWithDebInfo")
|
||||
|
||||
|
||||
@@ -310,18 +319,24 @@ cecho(f"* Build Directory = {BUILD_DIR}", MAGENTA)
|
||||
cecho(f"* Dependency Directory = {DEPS_DIR}", MAGENTA)
|
||||
cecho(f" - The directory where {PROJECT_NAME} dependencies are installed.")
|
||||
cecho(f"* Build Config Type = {BUILD_CFG}", MAGENTA)
|
||||
cecho(""" - The used build configuration type for the dependencies.
|
||||
Defaults to RelWithDebInfo if not specified.""")
|
||||
cecho(
|
||||
""" - The used build configuration type for the dependencies.
|
||||
Defaults to RelWithDebInfo if not specified."""
|
||||
)
|
||||
|
||||
if BUILD_CFG == "MinSizeRel":
|
||||
cecho(" WARNING: MinSizeRel build can suffer from a significant performance loss.", RED)
|
||||
|
||||
cecho(f"* IFCOS_NUM_BUILD_PROCS = {IFCOS_NUM_BUILD_PROCS}", MAGENTA)
|
||||
cecho(""" - How many compiler processes may be run in parallel.
|
||||
""")
|
||||
cecho(
|
||||
""" - How many compiler processes may be run in parallel.
|
||||
"""
|
||||
)
|
||||
cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA)
|
||||
cecho(""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
|
||||
""")
|
||||
cecho(
|
||||
""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
|
||||
"""
|
||||
)
|
||||
|
||||
dependency_tree: "dict[str, tuple[str, ...]]" = {
|
||||
"IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"),
|
||||
@@ -330,12 +345,13 @@ dependency_tree: "dict[str, tuple[str, ...]]" = {
|
||||
"OpenCOLLADA": ("libxml2", "pcre"),
|
||||
"IfcGeomServer": ("IfcGeom",),
|
||||
"IfcOpenShell-Python": ("python", "swig", "IfcGeom"),
|
||||
"swig": (),
|
||||
"swig": ("pcre2",),
|
||||
"boost": (),
|
||||
"libxml2": (),
|
||||
"python": (),
|
||||
"occ": (),
|
||||
"pcre": (),
|
||||
"pcre2": (),
|
||||
"json": (),
|
||||
"hdf5": (),
|
||||
"cgal": (),
|
||||
@@ -402,6 +418,7 @@ if WASM:
|
||||
"opencollada",
|
||||
"swig",
|
||||
"pcre",
|
||||
"pcre2",
|
||||
"IfcGeom",
|
||||
"IfcConvert",
|
||||
"IfcGeomServer",
|
||||
@@ -416,16 +433,13 @@ print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t
|
||||
|
||||
# Check that required tools are in PATH
|
||||
yacc = "yacc" # Used during swig building process, installed with `bison` on Debian / `byacc` on Red Hat.
|
||||
bison = "bison"
|
||||
|
||||
missing_commands: "list[str]" = []
|
||||
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz, bison]
|
||||
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz]
|
||||
if "wasm" in flags:
|
||||
# Skip swig build for WASM.
|
||||
required_commands.append("swig")
|
||||
required_commands.append("pyodide")
|
||||
required_commands.remove(yacc)
|
||||
required_commands.remove(bison)
|
||||
|
||||
for cmd in required_commands:
|
||||
if shutil.which(cmd) is None:
|
||||
@@ -484,7 +498,7 @@ def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool =
|
||||
collector.append(line)
|
||||
pipe.close()
|
||||
|
||||
logger.debug(f"running command `{' '.join(cmds)}` in directory '{cwd}'")
|
||||
logger.debug(f"running command {' '.join(cmds)} in directory {cwd}")
|
||||
stdout: list[str] = []
|
||||
stderr: list[str] = []
|
||||
|
||||
@@ -530,14 +544,14 @@ BOOST_LOCATION = f"https://github.com/boostorg/boost/releases/download/boost-{BO
|
||||
# Helper functions
|
||||
|
||||
|
||||
def run_autoconf(dependency_name: str, configure_args: "list[str]", cwd: str) -> None:
|
||||
def run_autoconf(arg1: str, configure_args: "list[str]", cwd: str) -> None:
|
||||
configure_path = os.path.realpath(os.path.join(cwd, "..", "configure"))
|
||||
if not os.path.exists(configure_path):
|
||||
run(
|
||||
[bash, "./autogen.sh"], cwd=os.path.realpath(os.path.join(cwd, ".."))
|
||||
) # only run autogen.sh in the directory it is located and use cwd to achieve that in order to not mess up things
|
||||
# Using `sh` over `bash` fixes issues with building swig
|
||||
prefix = os.path.realpath(f"{DEPS_DIR}/install/{dependency_name}")
|
||||
prefix = os.path.realpath(f"{DEPS_DIR}/install/{arg1}")
|
||||
|
||||
wasm = []
|
||||
if "wasm" in flags:
|
||||
@@ -916,15 +930,20 @@ if "pcre" in targets:
|
||||
restore_env("CC", OLD_CC)
|
||||
restore_env("CXX", OLD_CXX)
|
||||
|
||||
if "swig" in targets:
|
||||
dependency_name = f"swig-{SWIG_VERSION}"
|
||||
if "pcre2" in targets:
|
||||
build_dependency(
|
||||
name=dependency_name,
|
||||
mode="cmake",
|
||||
build_tool_args=[
|
||||
"-DWITH_PCRE=OFF",
|
||||
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}",
|
||||
],
|
||||
name=f"pcre2-{PCRE2_VERSION}",
|
||||
mode="autoconf",
|
||||
build_tool_args=[DISABLE_FLAG],
|
||||
download_url=f"https://downloads.sourceforge.net/project/pcre/pcre2/{PCRE2_VERSION}/",
|
||||
download_name=f"pcre2-{PCRE2_VERSION}.tar.bz2",
|
||||
)
|
||||
|
||||
if "swig" in targets:
|
||||
build_dependency(
|
||||
name=f"swig-{SWIG_VERSION}",
|
||||
mode="autoconf",
|
||||
build_tool_args=["--disable-ccache", f"--with-pcre2-prefix={DEPS_DIR}/install/pcre2-{PCRE2_VERSION}"],
|
||||
download_url="https://github.com/swig/swig.git",
|
||||
download_name="swig",
|
||||
download_tool=download_tool_git,
|
||||
@@ -932,18 +951,21 @@ if "swig" in targets:
|
||||
)
|
||||
|
||||
if USE_OCCT and "occ" in targets:
|
||||
occt_args: "list[str]" = []
|
||||
patches: "list[str]" = []
|
||||
patches = []
|
||||
if OCCT_VERSION < "7.4":
|
||||
patches.append("./patches/occt/enable-exception-handling.patch")
|
||||
|
||||
# Skip ExpToCasExe as we don't need it and it requires additional dependencies.
|
||||
# Before 7.7.2 ExpToCasExe is part of DataExchange, DETools doesn't exist yet.
|
||||
# Since we do need DataExchange (used for IgesSerializer), we use a patch to skip only ExpToCasExe.
|
||||
if "7.7.2" > OCCT_VERSION >= "7.7":
|
||||
if OCCT_VERSION == "7.7.1":
|
||||
patches.append("./patches/occt/no_ExpToCasExe.patch")
|
||||
elif OCCT_VERSION >= "7.7.2":
|
||||
occt_args.append("-DBUILD_MODULE_DETools=OFF")
|
||||
|
||||
if OCCT_VERSION == "7.7.2":
|
||||
patches.append("./patches/occt/no_ExpToCasExe_7_7_2.patch")
|
||||
|
||||
if OCCT_VERSION == "7.8.1":
|
||||
patches.append("./patches/occt/no_ExpToCasExe_7_8_1.patch")
|
||||
|
||||
if OCCT_VERSION == "7.9.1":
|
||||
patches.append("./patches/occt/no_ExpToCasExe_7_9_1.patch")
|
||||
|
||||
if "wasm" in flags:
|
||||
patches.append("./patches/occt/no_em_js.patch")
|
||||
@@ -964,7 +986,6 @@ if USE_OCCT and "occ" in targets:
|
||||
f"-DUSE_GLES2=OFF",
|
||||
f"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
|
||||
*MAC_CROSS_COMPILE_INTEL_ARGS,
|
||||
*occt_args,
|
||||
],
|
||||
download_url="https://github.com/Open-Cascade-SAS/OCCT",
|
||||
download_name="occt",
|
||||
@@ -1080,28 +1101,23 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"])
|
||||
|
||||
for PYTHON_VERSION in PYTHON_VERSIONS:
|
||||
# Don't fail silently on missing Python dependencies (e.g. openssl or zlib),
|
||||
# because later ifcopenshell-python build will fail too but in a more confusing way.
|
||||
build_dependency(
|
||||
f"python-{PYTHON_VERSION}",
|
||||
"autoconf",
|
||||
PYTHON_CONFIGURE_ARGS,
|
||||
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"
|
||||
# `_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."
|
||||
build_dependency(
|
||||
f"python-{PYTHON_VERSION}",
|
||||
"autoconf",
|
||||
PYTHON_CONFIGURE_ARGS,
|
||||
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
|
||||
f"Python-{PYTHON_VERSION}.tgz",
|
||||
)
|
||||
raise
|
||||
except RuntimeError as e:
|
||||
# Sometimes setting up modules such as pip/lzma can cause
|
||||
# the python installer script to return a non zero exit
|
||||
# code where actually the headers and dynamic libraries
|
||||
# are installed correctly. This is all we need so we catch
|
||||
# the exception and only reraise if a partially successful
|
||||
# install is not detected.
|
||||
if not os.path.exists(os.path.join(DEPS_DIR, "install", f"python-{PYTHON_VERSION}")):
|
||||
raise e
|
||||
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
assert original_path
|
||||
@@ -1519,16 +1535,13 @@ 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
|
||||
# otherwise the wheel will use version and dependencies from toml, not setup.py.
|
||||
(REPO_PATH / "pyproject.toml").write_text("")
|
||||
|
||||
elif USE_CURRENT_PYTHON_VERSION:
|
||||
python_info = sysconfig.get_paths()
|
||||
compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable)
|
||||
else:
|
||||
for python_version in PYTHON_VERSIONS:
|
||||
python_path = INSTALL_DIR / f"python-{python_version}"
|
||||
python_path = Path(DEPS_DIR) / "install" / f"python-{python_version}"
|
||||
module_dir = compile_python_wrapper(python_version, python_path=python_path)
|
||||
assert module_dir
|
||||
# Not sure why, but added after reading this in the logs
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
--- a/adm/MODULES
|
||||
+++ b/adm/MODULES
|
||||
@@ -3,5 +3,5 @@ ModelingData TKG2d TKG3d TKGeomBase TKBRep
|
||||
ModelingAlgorithms TKGeomAlgo TKTopAlgo TKPrim TKBO TKBool TKHLR TKFillet TKOffset TKFeat TKMesh TKXMesh TKShHealing
|
||||
Visualization TKService TKV3d TKOpenGl TKOpenGles TKMeshVS TKIVtk TKD3DHost
|
||||
ApplicationFramework TKCDF TKLCAF TKCAF TKBinL TKXmlL TKBin TKXml TKStdL TKStd TKTObj TKBinTObj TKXmlTObj TKVCAF
|
||||
-DataExchange TKXDE TKXSBase TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP TKIGES TKXCAF TKXDEIGES TKXDESTEP TKSTL TKVRML TKXmlXCAF TKBinXCAF TKRWMesh TKXDECascade TKExpress ExpToCasExe
|
||||
+DataExchange TKXDE TKXSBase TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP TKIGES TKXCAF TKXDEIGES TKXDESTEP TKSTL TKVRML TKXmlXCAF TKBinXCAF TKRWMesh TKXDECascade TKExpress
|
||||
Draw TKDraw TKTopTest TKOpenGlTest TKOpenGlesTest TKD3DHostTest TKViewerTest TKXSDRAW TKDCAF TKXDEDRAW TKTObjDRAW TKQADraw TKIVtkDraw DRAWEXE
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index fd17283f77..6cecf9dad3 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -826,6 +826,8 @@ if (EMSCRIPTEN)
|
||||
list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
|
||||
endif()
|
||||
|
||||
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
|
||||
+
|
||||
# bison
|
||||
if (BUILD_YACCLEX)
|
||||
OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison")
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 1bacca1a48..11f931ad39 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -820,6 +820,8 @@ else()
|
||||
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
|
||||
endif()
|
||||
|
||||
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
|
||||
+
|
||||
# bison
|
||||
if (BUILD_YACCLEX)
|
||||
OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison")
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 86905287dc..9d0bce984c 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -828,6 +828,8 @@ else()
|
||||
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
|
||||
endif()
|
||||
|
||||
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
|
||||
+
|
||||
# bison
|
||||
if (BUILD_YACCLEX)
|
||||
OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison")
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 34300d41ad..09b2e0d45f 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -721,6 +721,8 @@ else()
|
||||
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
|
||||
endif()
|
||||
|
||||
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
|
||||
+
|
||||
# bison
|
||||
if (BUILD_YACCLEX)
|
||||
list (APPEND OCCT_3RDPARTY_CMAKE_LIST "adm/cmake/bison")
|
||||
+11
-15
@@ -1,30 +1,26 @@
|
||||
#!/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.
|
||||
|
||||
# Install uv.
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
uv venv --python 3.13 --clear
|
||||
uv venv --python 3.13
|
||||
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.
|
||||
git clone https://github.com/emscripten-core/emsdk
|
||||
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,5 +1,3 @@
|
||||
# /// script
|
||||
# ///
|
||||
"""
|
||||
Cache built dependencies for builds.
|
||||
|
||||
@@ -7,13 +5,9 @@ This script is finding common install directory and either
|
||||
packs each folder into a tar.gz archive, if it wasn't packed before,
|
||||
or unpacks existing archives.
|
||||
|
||||
Expected to be executed from 'build' directory (e.g. that might contain 'Linux/x86_64/install').
|
||||
|
||||
Usage: python cache_dependencies.py [pack|unpack]
|
||||
"""
|
||||
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
@@ -23,35 +17,23 @@ CACHE_PREFIX = "cache-"
|
||||
|
||||
|
||||
def get_install_dir() -> Path:
|
||||
if platform.system() == "Darwin":
|
||||
pattern = "Darwin/*/*/install"
|
||||
else:
|
||||
pattern = "*/*/install"
|
||||
for data in Path.cwd().glob(pattern):
|
||||
for data in Path.cwd().glob("*/*/install"):
|
||||
return data
|
||||
raise Exception("No install dir found")
|
||||
|
||||
|
||||
def run(cmd: str) -> None:
|
||||
print(f"Running command: `{cmd}`")
|
||||
subprocess.check_call(cmd, shell=True)
|
||||
|
||||
|
||||
def pack_dependencies(install_dir: Path) -> None:
|
||||
# Process each install_dir
|
||||
for dependency_path in install_dir.iterdir():
|
||||
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}'")
|
||||
else:
|
||||
# Python's `tarfile` is 10x slower than `tar` cli, so we use `tar`.
|
||||
run(f'tar -czf "{tar_path}" -C "{install_dir}" "{dependency_name}"')
|
||||
with tarfile.open(tar_path, "w:gz") as tar:
|
||||
tar.add(dependency_path, arcname=dependency_path.name)
|
||||
print(f"Created cache: '{tar_path}'")
|
||||
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
+9
-189
@@ -2,11 +2,10 @@
|
||||
name = "IfcOpenShell"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"black==26.3.1",
|
||||
"ruff==0.15.12",
|
||||
"black==26.1.0",
|
||||
"ruff==0.15.1",
|
||||
"poethepoet",
|
||||
"ty==0.0.32",
|
||||
"gersemi==0.26.1",
|
||||
"gersemi==0.25.4",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
@@ -16,8 +15,7 @@ include = '''
|
||||
|nix/.*.pyi?$
|
||||
'''
|
||||
extend-exclude = '''
|
||||
src/ifcopenshell-python/ifcopenshell/express/rules/*
|
||||
|src/ifcopenshell-python/ifcopenshell/express/express_parser.py
|
||||
src/ifcopenshell-python/ifcopenshell/express/*
|
||||
|src/ifcopenshell-python/ifcopenshell/mvd/*
|
||||
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|
||||
|src/ifc2ca/templates/*
|
||||
@@ -29,15 +27,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
|
||||
exclude = [
|
||||
"_deps",
|
||||
]
|
||||
|
||||
# Define here general ruff settings,
|
||||
# then they will be inherited by projects' .toml files.
|
||||
@@ -82,184 +71,15 @@ ignore = [
|
||||
"UP032", # Replace .format with f-string
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
all = "ignore"
|
||||
|
||||
# Structural rules (no deep type inference needed, easier to adapt).
|
||||
abstract-method-in-final-class = "error"
|
||||
ambiguous-protocol-member = "error"
|
||||
conflicting-declarations = "error"
|
||||
conflicting-metaclass = "error"
|
||||
cyclic-class-definition = "error"
|
||||
cyclic-type-alias-definition = "error"
|
||||
dataclass-field-order = "error"
|
||||
duplicate-base = "error"
|
||||
duplicate-kw-only = "error"
|
||||
empty-body = "error"
|
||||
escape-character-in-forward-annotation = "error"
|
||||
final-on-non-method = "error"
|
||||
final-without-value = "error"
|
||||
ignore-comment-unknown-rule = "error"
|
||||
implicit-concatenated-string-type-annotation = "error"
|
||||
inconsistent-mro = "error"
|
||||
ineffective-final = "error"
|
||||
instance-layout-conflict = "error"
|
||||
invalid-dataclass = "error"
|
||||
invalid-dataclass-override = "error"
|
||||
invalid-enum-member-annotation = "error"
|
||||
invalid-explicit-override = "error"
|
||||
invalid-frozen-dataclass-subclass = "error"
|
||||
invalid-generic-class = "error"
|
||||
invalid-generic-enum = "error"
|
||||
invalid-ignore-comment = "error"
|
||||
invalid-legacy-positional-parameter = "error"
|
||||
invalid-legacy-type-variable = "error"
|
||||
invalid-named-tuple = "error"
|
||||
invalid-newtype = "error"
|
||||
invalid-overload = "error"
|
||||
invalid-paramspec = "error"
|
||||
invalid-protocol = "error"
|
||||
invalid-syntax-in-forward-annotation = "error"
|
||||
invalid-total-ordering = "error"
|
||||
invalid-type-alias-type = "error"
|
||||
invalid-type-checking-constant = "error"
|
||||
invalid-type-guard-definition = "error"
|
||||
invalid-type-variable-bound = "error"
|
||||
invalid-type-variable-constraints = "error"
|
||||
invalid-typed-dict-header = "error"
|
||||
invalid-typed-dict-statement = "error"
|
||||
override-of-final-method = "error"
|
||||
override-of-final-variable = "error"
|
||||
possibly-missing-import = "error"
|
||||
possibly-missing-submodule = "error"
|
||||
# Has false positives due to ty walrus operator bug.
|
||||
# possibly-unresolved-reference = "error"
|
||||
raw-string-type-annotation = "error"
|
||||
redundant-final-classvar = "error"
|
||||
shadowed-type-variable = "error"
|
||||
subclass-of-final-class = "error"
|
||||
super-call-in-named-tuple-method = "error"
|
||||
unavailable-implicit-super-arguments = "error"
|
||||
unbound-type-variable = "error"
|
||||
undefined-reveal = "error"
|
||||
unresolved-global = "error"
|
||||
unresolved-import = "error"
|
||||
unresolved-reference = "error"
|
||||
unused-ignore-comment = "error"
|
||||
unused-type-ignore-comment = "error"
|
||||
useless-overload-body = "error"
|
||||
|
||||
# Non-structural rules:
|
||||
deprecated = "error"
|
||||
zero-stepsize-in-slice = "error"
|
||||
possibly-missing-implicit-call = "error"
|
||||
unused-awaitable = "error"
|
||||
|
||||
# Function argument rules:
|
||||
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
|
||||
# call-non-callable = "error"
|
||||
conflicting-argument-forms = "error"
|
||||
# Too many false positives.
|
||||
# invalid-argument-type = "error"
|
||||
missing-argument = "error"
|
||||
parameter-already-assigned = "error"
|
||||
positional-only-parameter-as-kwarg = "error"
|
||||
too-many-positional-arguments = "error"
|
||||
unknown-argument = "error"
|
||||
# Has a lot of warnings due to current ty walrus operator issues.
|
||||
# index-out-of-bounds = "error"
|
||||
# unresolved-attribute = "error"
|
||||
|
||||
[tool.ty.environment]
|
||||
extra-paths = [
|
||||
"src/bonsai/external_dependencies",
|
||||
"src/bcf",
|
||||
"src/bsdd",
|
||||
"src/bonsai",
|
||||
"src/ifc4d",
|
||||
"src/ifc5d",
|
||||
"src/ifccityjson",
|
||||
"src/ifcclash",
|
||||
"src/ifccsv",
|
||||
"src/ifcdiff",
|
||||
"src/ifcfm",
|
||||
"src/ifcopenshell-python",
|
||||
"src/ifcpatch",
|
||||
"src/ifctester",
|
||||
]
|
||||
|
||||
[tool.ty.src]
|
||||
exclude = [
|
||||
# External dependencies cloned for type checking only.
|
||||
"src/bonsai/external_dependencies",
|
||||
# Submodules.
|
||||
"src/ifcopenshell-python/ifcopenshell/express",
|
||||
"src/ifcopenshell-python/ifcopenshell/mvd",
|
||||
"src/ifcopenshell-python/ifcopenshell/simple_spf",
|
||||
"src/svgfill/3rdparty",
|
||||
# Has special dependencies.
|
||||
"src/ifcopenshell-python/ifcopenshell/geom/app.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/geom/code_editor_pane.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/doc.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/ifc4x3dev_scrape_data_for_docs.py",
|
||||
# Too esoteric.
|
||||
"src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py",
|
||||
"src/ifc2ca/templates",
|
||||
# Too dev.
|
||||
"src/bcf/setup.py",
|
||||
"src/bsdd/yml_to_classes.py",
|
||||
# Deprecated.
|
||||
"src/ifc2ca/_deprecated",
|
||||
]
|
||||
|
||||
[tool.poe.tasks]
|
||||
|
||||
ruff = "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]
|
||||
# --ignore unresolved-reference: walrus operator false positives in ty.
|
||||
cmd = """
|
||||
ty check
|
||||
src/bcf
|
||||
src/bsdd
|
||||
src/ifc2ca
|
||||
src/ifc4d
|
||||
src/ifc5d
|
||||
src/ifccityjson
|
||||
src/ifcclash
|
||||
src/ifccsv
|
||||
src/ifcdiff
|
||||
src/ifcfm
|
||||
src/ifcopenshell-python
|
||||
src/ifcpatch
|
||||
src/ifctester
|
||||
--python=src/ifcopenshell-python/.venv
|
||||
--ignore unresolved-reference
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.bonsai-deps]
|
||||
help = "Clone or update Bonsai external dependencies."
|
||||
cmd = "python src/bonsai/scripts/bonsai_deps.py"
|
||||
|
||||
@@ -34,8 +34,8 @@ client_id, client_secret = "", ""
|
||||
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||
self.server.auth_code = query.get("code", [""])[0]
|
||||
self.server.auth_state = query.get("state", [""])[0]
|
||||
self.server.auth_code = query.get("code", [""])[0] # type:ignore
|
||||
self.server.auth_state = query.get("state", [""])[0] # type:ignore
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/plain")
|
||||
self.end_headers()
|
||||
@@ -255,7 +255,7 @@ class BcfClient:
|
||||
project_id: str = "",
|
||||
topics: str = "",
|
||||
query_string: Optional[str] = None,
|
||||
) -> None:
|
||||
) -> list[Any]:
|
||||
# return self.get(
|
||||
# f"/projects/{project_id}/topics",
|
||||
# {
|
||||
|
||||
@@ -173,17 +173,16 @@ def assert_viewpoints(viewpoints):
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
|
||||
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=False,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=False,
|
||||
),
|
||||
exceptions=expected_exception,
|
||||
default_visibility=False,
|
||||
),
|
||||
@@ -194,7 +193,6 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
|
||||
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
|
||||
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
||||
field_of_view=60,
|
||||
aspect_ratio=1.0,
|
||||
),
|
||||
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
||||
)
|
||||
@@ -202,17 +200,16 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
|
||||
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=True,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=True,
|
||||
),
|
||||
exceptions=expected_exception,
|
||||
default_visibility=True,
|
||||
),
|
||||
@@ -223,7 +220,6 @@ def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, ex
|
||||
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
|
||||
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
||||
field_of_view=60,
|
||||
aspect_ratio=1.0,
|
||||
),
|
||||
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
||||
)
|
||||
|
||||
+30
-20
@@ -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:=e8eb5e4
|
||||
.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
|
||||
@@ -229,8 +223,19 @@ endif
|
||||
cd build/bonsai/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
|
||||
|
||||
# Provides IFCJSON functionality
|
||||
# TODO: Use official repo, once https://github.com/IFCJSON-Team/IFC2JSON_python/pull/8 is merged.
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/IFC2JSON_python.git@pyproject_toml" --no-deps -w wheels/
|
||||
cd build && wget -O ifc2json.zip https://github.com/IFCJSON-Team/IFC2JSON_python/archive/refs/heads/master.zip
|
||||
cd build && unzip ifc2json.zip && rm ifc2json.zip
|
||||
# IFCJSON doesn't have pyproject.toml, so we use python command.
|
||||
cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-*/file_converters && \
|
||||
$(PYTHON) -c "from setuptools import setup; \
|
||||
setup( \
|
||||
name='ifcjson', \
|
||||
version='0.0.1', \
|
||||
author='Jan Brouwer', \
|
||||
author_email='jan@brewsky.nl', \
|
||||
packages=['ifcjson'], \
|
||||
)" bdist_wheel
|
||||
cp -r build/IFC2JSON_python-*/file_converters/dist/*.whl build/wheels/
|
||||
|
||||
# Brickschema requires pkg_resources which is provided by Blender.
|
||||
# Provides Brickschema functionality
|
||||
@@ -238,16 +243,26 @@ 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/
|
||||
cd build && wget https://github.com/prochitecture/bpypolyskel/archive/refs/heads/master.zip
|
||||
cd build && unzip master.zip && rm master.zip
|
||||
cd build && . env/$(VENV_ACTIVATE) && cd bpypolyskel-master && \
|
||||
$(PYTHON) -c "from setuptools import setup; \
|
||||
setup( \
|
||||
name='bpypolyskel', \
|
||||
version='0.0.0', \
|
||||
packages=['bpypolyskel'], \
|
||||
)" bdist_wheel
|
||||
cp -r build/bpypolyskel-master/dist/*.whl build/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 +281,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 +374,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
|
||||
|
||||
@@ -34,16 +34,17 @@ IN_PACKAGE = __package__ == "bonsai"
|
||||
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import traceback
|
||||
import uuid
|
||||
import webbrowser
|
||||
from collections import deque
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
from typing import Any, Union
|
||||
|
||||
last_commit_hash = "8888888"
|
||||
last_commit_date = "9999999"
|
||||
last_git_branch = "7777777"
|
||||
|
||||
|
||||
def get_last_commit_hash() -> Union[str, None]:
|
||||
@@ -61,15 +62,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] = {}
|
||||
|
||||
@@ -81,21 +73,6 @@ REINSTALLED_BBIM_VERSION: Union[str, None] = None
|
||||
REGISTERED_BBIM_PACKAGE: str
|
||||
|
||||
|
||||
def is_registering() -> bool:
|
||||
"""
|
||||
During addon registration ``bpy.context`` and ``bpy.data`` are restricted
|
||||
and you can't access their properties.
|
||||
"""
|
||||
import bpy
|
||||
|
||||
if TYPE_CHECKING or bpy.app.version >= (5, 0, 0):
|
||||
import _bpy_restrict_state as bpy_restrict_state
|
||||
else:
|
||||
import bpy_restrict_state
|
||||
|
||||
return isinstance(bpy.context, bpy_restrict_state._RestrictContext)
|
||||
|
||||
|
||||
def initialize_bbim_semver():
|
||||
"""Initialize `bbim_semver` dictionary.
|
||||
|
||||
@@ -117,13 +94,9 @@ def initialize_bbim_semver():
|
||||
bbim_semver["version"] = version_str
|
||||
|
||||
|
||||
def get_debug_info(*, bonsai_failed_to_load: bool = False) -> dict[str, Any]:
|
||||
import bpy
|
||||
|
||||
def get_debug_info():
|
||||
bbim_version = bbim_semver["version"]
|
||||
|
||||
# All data here should be gettable even in case of `bpy.context` and `bpy.data` being inaccessible
|
||||
# and Bonsai completely failed to load.
|
||||
debug_info = {
|
||||
"os": platform.system(),
|
||||
"os_version": platform.version(),
|
||||
@@ -135,19 +108,10 @@ 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,
|
||||
}
|
||||
|
||||
# Can't access blend data or context during registration.
|
||||
# If Bonsai failed to load we cannot safely access any of its properties or its tools
|
||||
# as they may not be registered yet and acessing them will break Bonsai Fatal Error UI.
|
||||
if is_registering() or bonsai_failed_to_load:
|
||||
return debug_info
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
# Add .blend file save information
|
||||
if bpy.data.is_saved:
|
||||
debug_info["blend_file_path"] = bpy.data.filepath
|
||||
@@ -168,7 +132,7 @@ def get_debug_info(*, bonsai_failed_to_load: bool = False) -> dict[str, Any]:
|
||||
return debug_info
|
||||
|
||||
|
||||
def format_debug_info(info: dict[str, Any]) -> str:
|
||||
def format_debug_info(info: dict):
|
||||
last_actions = ""
|
||||
for action in info["last_actions"]:
|
||||
last_actions += f"\n# {action['type']}: {action['name']}"
|
||||
@@ -186,10 +150,33 @@ def get_binaries(path: Path) -> Generator[Path, None, None]:
|
||||
yield from path.glob("**/*.so")
|
||||
|
||||
|
||||
# TODO: remove before 0.8.6 release.
|
||||
# On Windows issues with removing extensions were resolved in Blender 4.3,
|
||||
# but we removed our workaround that was producing some junk only in 0.8.5 release.
|
||||
# So we're temporarily keeping the part that's cleaning up outputs from previous releases.
|
||||
def safe_link_dlls() -> None:
|
||||
# Blender 4.2+ has a problem on Windows for disabling/enabling/reinstalling extensions
|
||||
# with loaded binary dependencies (on Windows you can't remove a binary if it's loaded by some program).
|
||||
# To avoid this issue we temporary hard link dlls to our temp directory on unregister()
|
||||
# (unregister is executed before Blender will try to uninstall dependencies and the issue will arise).
|
||||
# Then, Blender won't have a problem unlinking unloaded dlls as they are still linked somewhere.
|
||||
# On register() we clean up our temp directory with binaries.
|
||||
#
|
||||
# TODO: If user uninstalls Bonsai to never use it again, temporary directory won't be cleared.
|
||||
#
|
||||
# See: https://projects.blender.org/blender/blender/issues/125049
|
||||
import bpy
|
||||
|
||||
ext_path = Path(bpy.utils.user_resource("EXTENSIONS"))
|
||||
local_path = ext_path / ".local"
|
||||
|
||||
# We use random hash subfolder as user may try to enable/disable addon multiple times.
|
||||
random_hash = uuid.uuid4().hex[:8]
|
||||
temp_local = ext_path / ".local_temp" / random_hash
|
||||
temp_local.mkdir(parents=True)
|
||||
|
||||
for filepath in get_binaries(local_path):
|
||||
dest_path = temp_local / filepath.relative_to(local_path)
|
||||
dest_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
os.link(filepath, dest_path)
|
||||
|
||||
|
||||
def clean_up_dlls_safe_links() -> None:
|
||||
import bpy
|
||||
|
||||
@@ -219,8 +206,6 @@ def clean_up_dlls_safe_links() -> None:
|
||||
|
||||
|
||||
if IN_BLENDER:
|
||||
import bpy
|
||||
|
||||
initialize_bbim_semver()
|
||||
|
||||
def get_binary_info() -> dict[str, Any]:
|
||||
@@ -262,12 +247,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
|
||||
|
||||
@@ -314,6 +297,9 @@ if IN_BLENDER:
|
||||
purge_cache()
|
||||
|
||||
def unregister():
|
||||
if platform.system() == "Windows":
|
||||
safe_link_dlls()
|
||||
|
||||
import bonsai.bim
|
||||
|
||||
bonsai.bim.unregister()
|
||||
@@ -348,7 +334,7 @@ if IN_BLENDER:
|
||||
bl_context = "scene"
|
||||
|
||||
def draw(self, context):
|
||||
info = get_debug_info(bonsai_failed_to_load=True)
|
||||
info = get_debug_info()
|
||||
|
||||
layout = self.layout
|
||||
layout.alert = True
|
||||
@@ -424,7 +410,7 @@ if IN_BLENDER:
|
||||
bl_description = "Copies debugging information to your clipboard for use in bugreports"
|
||||
|
||||
def execute(self, context):
|
||||
info = get_debug_info(bonsai_failed_to_load=True)
|
||||
info = get_debug_info()
|
||||
info.update(get_binary_info())
|
||||
info = format_debug_info(info)
|
||||
context.window_manager.clipboard = info
|
||||
|
||||
@@ -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
|
||||
@@ -184,6 +184,8 @@ classes = [
|
||||
ui.BIM_PT_tab_materials,
|
||||
ui.BIM_PT_tab_styles,
|
||||
ui.BIM_PT_tab_profiles,
|
||||
# Civil infrastructure
|
||||
ui.BIM_PT_tab_horizontal_alignment,
|
||||
# Drawings and documents
|
||||
ui.BIM_PT_tab_sheets,
|
||||
ui.BIM_PT_tab_drawings,
|
||||
@@ -268,8 +270,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 +327,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
|
||||
|
||||
@@ -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));
|
||||
#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,7 +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.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -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
|
||||
@@ -24,14 +24,21 @@ import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from logging import Logger
|
||||
from math import radians
|
||||
from typing import Union
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.unit
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.core.aggregate
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.spatial
|
||||
import bonsai.core.style
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
@@ -72,7 +79,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,
|
||||
@@ -437,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(
|
||||
@@ -451,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()
|
||||
@@ -491,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()
|
||||
@@ -515,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()
|
||||
|
||||
@@ -28,6 +28,7 @@ import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.doc import (
|
||||
get_attribute_doc,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -32,6 +32,7 @@ import ifcopenshell.api.pset
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape
|
||||
@@ -64,8 +65,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
|
||||
|
||||
@@ -84,7 +85,7 @@ class MaterialCreator:
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
self.parse_element_type_material_styles(element)
|
||||
self.parsed_meshes.add(self.mesh.name)
|
||||
if self.ifc_import_settings.load_indexed_maps:
|
||||
if not self.ifc_import_settings.load_indexed_maps:
|
||||
self.load_texture_maps(shape_has_openings)
|
||||
self.assign_material_slots_to_faces()
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
@@ -116,6 +117,7 @@ class MaterialCreator:
|
||||
for texture in texture_style.Textures or []:
|
||||
if coords := getattr(texture, "IsMappedBy", None):
|
||||
coords = coords[0]
|
||||
# IfcTextureCoordinateGenerator handled in the style shader graph
|
||||
if coords.is_a("IfcIndexedTextureMap"):
|
||||
return coords
|
||||
# TODO: support IfcTextureMap
|
||||
@@ -133,10 +135,6 @@ class MaterialCreator:
|
||||
if shape_has_openings and coords.is_a("IfcIndexedTextureMap"):
|
||||
continue
|
||||
tool.Loader.load_indexed_map(coords, self.mesh)
|
||||
elif tool.Style.get_texture_style(material):
|
||||
# No explicit coordinate mapping (e.g. IFC2X3 has no IsMappedBy,
|
||||
# and IFC4 COORD uses generated UVs). Bake XY→UV as fallback.
|
||||
tool.Loader.load_generated_uv_map(self.mesh)
|
||||
|
||||
def assign_material_slots_to_faces(self) -> None:
|
||||
if not self.mesh["ios_materials"]:
|
||||
@@ -223,7 +221,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] = {}
|
||||
@@ -748,7 +745,6 @@ class IfcImporter:
|
||||
self.update_progress((percent_average / 100 * progress_range) + start_progress)
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
product = self.file.by_id(shape.id)
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
@@ -896,6 +892,48 @@ class IfcImporter:
|
||||
obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element))
|
||||
)
|
||||
|
||||
if element.is_a("IfcAnnotation") and getattr(element, "ObjectType", None) == "IMAGE":
|
||||
image = None
|
||||
if obj.data and obj.data.materials and obj.data.materials[0]:
|
||||
material = obj.data.materials[0]
|
||||
if material.use_nodes and material.node_tree:
|
||||
for node in material.node_tree.nodes:
|
||||
if node.type == "TEX_IMAGE" and node.image:
|
||||
image = node.image
|
||||
break
|
||||
if image:
|
||||
import bmesh
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
if not bm.loops.layers.uv:
|
||||
uv_layer = bm.loops.layers.uv.new()
|
||||
else:
|
||||
uv_layer = bm.loops.layers.uv.active
|
||||
|
||||
if bm.verts:
|
||||
min_x = min(v.co.x for v in bm.verts)
|
||||
max_x = max(v.co.x for v in bm.verts)
|
||||
min_y = min(v.co.y for v in bm.verts)
|
||||
max_y = max(v.co.y for v in bm.verts)
|
||||
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
for face in bm.faces:
|
||||
for loop in face.loops:
|
||||
vert = loop.vert
|
||||
u = (vert.co.x - min_x) / width if width > 0 else 0.5
|
||||
v = (vert.co.y - min_y) / height if height > 0 else 0.5
|
||||
|
||||
u = max(0.0, min(1.0, u))
|
||||
v = max(0.0, min(1.0, v))
|
||||
|
||||
loop[uv_layer].uv = (u, v)
|
||||
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
obj.data.update()
|
||||
return obj
|
||||
|
||||
def load_existing_meshes(self) -> None:
|
||||
@@ -1022,8 +1060,7 @@ class IfcImporter:
|
||||
obj.hide_select = True
|
||||
obj.hide_viewport = True
|
||||
self.project["blender"].objects.link(obj)
|
||||
collection_props = tool.Blender.get_collection_props(self.project["blender"])
|
||||
collection_props.obj = obj
|
||||
self.project["blender"].BIMCollectionProperties.obj = obj
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.collection = self.collections[project.GlobalId] = self.project["blender"]
|
||||
|
||||
@@ -1220,18 +1257,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
|
||||
|
||||
@@ -19,12 +19,21 @@
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import ifcopenshell
|
||||
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
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
def create_bounding_box(objs):
|
||||
@@ -72,8 +81,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 +155,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 +193,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 +227,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":
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.group
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
|
||||
@@ -23,7 +23,12 @@ import ifcopenshell.util.element
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
@@ -32,6 +37,8 @@ from bonsai.bim.module.aggregate.decorator import (
|
||||
AggregateDecorator,
|
||||
AggregateModeDecorator,
|
||||
)
|
||||
from bonsai.bim.module.spatial.data import SpatialData
|
||||
from bonsai.bim.prop import Attribute, StrProperty
|
||||
|
||||
|
||||
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
|
||||
@@ -73,22 +80,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 +96,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 +127,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 +143,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
|
||||
|
||||
@@ -21,6 +21,7 @@ from bpy.types import Panel
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.aggregate.data import AggregateData
|
||||
from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData
|
||||
|
||||
|
||||
class BIM_PT_aggregate(Panel):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2026 Michael Yoder <myoder@desertspringscivil.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
@@ -17,11 +17,37 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from . import ui, prop, operator, decorator
|
||||
|
||||
# from . import ui, prop, operator
|
||||
from . import operator
|
||||
|
||||
classes = (operator.ImportAlignmentCSV,)
|
||||
classes = (
|
||||
# Property groups (must be registered before classes that use them)
|
||||
prop.AlignmentPI,
|
||||
prop.AlignmentDisplayRow,
|
||||
prop.CivilAlignmentProperties,
|
||||
# UILists
|
||||
ui.CIVIL_UL_alignment_pis,
|
||||
operator.ImportAlignmentCSV,
|
||||
# Operators - PI Management
|
||||
operator.CIVIL_OT_add_pi,
|
||||
operator.CIVIL_OT_remove_pi,
|
||||
operator.CIVIL_OT_pick_pi_from_viewport,
|
||||
operator.CIVIL_OT_recalculate_pis,
|
||||
operator.CIVIL_OT_clear_pis,
|
||||
# Operators - Creation
|
||||
operator.CIVIL_OT_create_alignment_by_pi,
|
||||
operator.CIVIL_OT_import_alignment_csv,
|
||||
# Operators - Stationing
|
||||
operator.CIVIL_OT_add_stationing_referent,
|
||||
operator.CIVIL_OT_name_segments,
|
||||
# Operators - PI Edit Mode
|
||||
operator.CIVIL_OT_enter_pi_edit_mode,
|
||||
# UI Panels (appear in Properties sidebar under CIVIL tab)
|
||||
ui.CIVIL_PT_alignment_creation,
|
||||
ui.CIVIL_PT_pi_editor,
|
||||
ui.CIVIL_PT_alignment_stationing,
|
||||
)
|
||||
|
||||
|
||||
def menu_func_import(self, context):
|
||||
@@ -29,8 +55,10 @@ def menu_func_import(self, context):
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.CivilAlignmentProperties = bpy.props.PointerProperty(type=prop.CivilAlignmentProperties)
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||
del bpy.types.Scene.CivilAlignmentProperties
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.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/>.
|
||||
|
||||
|
||||
"""Data caching layer for the alignment module
|
||||
|
||||
This module provides cached access to alignment data for UI display,
|
||||
following Bonsai's data loading pattern.
|
||||
"""
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class AlignmentData:
|
||||
"""Cached alignment data for UI display"""
|
||||
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""Load alignment data from IFC file"""
|
||||
cls.data = {
|
||||
"alignments": [],
|
||||
"active_alignment": None,
|
||||
"segments": [],
|
||||
}
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
if ifc is None:
|
||||
cls.is_loaded = True
|
||||
return
|
||||
|
||||
# Load all alignments
|
||||
alignments = ifc.by_type("IfcAlignment")
|
||||
cls.data["alignments"] = [
|
||||
{
|
||||
"id": a.id(),
|
||||
"name": a.Name or f"Alignment {a.id()}",
|
||||
"global_id": a.GlobalId,
|
||||
}
|
||||
for a in alignments
|
||||
]
|
||||
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def refresh(cls):
|
||||
"""Force refresh of alignment data"""
|
||||
cls.is_loaded = False
|
||||
cls.load()
|
||||
@@ -0,0 +1,193 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.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/>.
|
||||
|
||||
"""Alignment module decorators for GPU visualization.
|
||||
|
||||
This module contains decorators for rendering visual feedback during
|
||||
alignment-related operations, such as PI editing.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
class PIEditDecorator:
|
||||
"""Decorator for visualizing PI edit mode.
|
||||
|
||||
This decorator provides visual feedback while the user is editing
|
||||
PI (Point of Intersection) positions with standard Blender transform tools:
|
||||
- Yellow lines connecting PI empties (tangent preview)
|
||||
- HUD text showing instructions
|
||||
|
||||
The decorator reads positions directly from the PI empty objects,
|
||||
which are updated by Blender's transform operators (G key).
|
||||
"""
|
||||
|
||||
# Class-level state (cleared on uninstall)
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
# References to PI empty objects
|
||||
pi_empties = []
|
||||
|
||||
# Colors
|
||||
COLOR_TANGENT_LINE = (1.0, 0.9, 0.2, 1.0) # Yellow for tangent lines
|
||||
COLOR_HUD_TEXT = (1.0, 1.0, 1.0, 1.0) # White for HUD text
|
||||
COLOR_EDIT_MODE_BG = (0.2, 0.4, 0.8, 0.8) # Blue tint for edit mode indicator
|
||||
|
||||
# Drawing parameters
|
||||
LINE_WIDTH = 2.5
|
||||
|
||||
@classmethod
|
||||
def install(cls, context, pi_empties):
|
||||
"""Install decorator handlers for PI edit mode visualization.
|
||||
|
||||
Args:
|
||||
context: Blender context
|
||||
pi_empties: List of PI EMPTY objects to visualize
|
||||
"""
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
|
||||
cls.pi_empties = pi_empties
|
||||
|
||||
handler = cls()
|
||||
# POST_VIEW for 3D world-space drawing (tangent lines in 3D)
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_tangent_lines_3d, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
# POST_PIXEL for 2D screen-space drawing (HUD)
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_hud, (context,), "WINDOW", "POST_PIXEL")
|
||||
)
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
"""Remove all handlers and clear state."""
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.handlers = []
|
||||
cls.is_installed = False
|
||||
cls.pi_empties = []
|
||||
|
||||
@classmethod
|
||||
def update_positions(cls, pi_empties):
|
||||
"""Update the list of PI empties (called when positions change).
|
||||
|
||||
Args:
|
||||
pi_empties: Updated list of PI EMPTY objects
|
||||
"""
|
||||
cls.pi_empties = pi_empties
|
||||
|
||||
def draw_batch_3d(self, shader_type, content_pos, color, indices=None):
|
||||
"""Draw a batch of 3D primitives using GPU shader.
|
||||
|
||||
Args:
|
||||
shader_type: Type of primitive ("LINES", "POINTS", etc.)
|
||||
content_pos: List of 3D vertex positions
|
||||
color: RGBA color tuple
|
||||
indices: Optional list of index pairs for lines
|
||||
"""
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
shader.bind()
|
||||
|
||||
# Get viewport size from active region
|
||||
region = bpy.context.region
|
||||
shader.uniform_float("viewportSize", (region.width, region.height))
|
||||
shader.uniform_float("lineWidth", self.LINE_WIDTH)
|
||||
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_tangent_lines_3d(self, context):
|
||||
"""Draw yellow tangent lines connecting PI empties in 3D space."""
|
||||
if not self.pi_empties or len(self.pi_empties) < 2:
|
||||
return
|
||||
|
||||
# Collect 3D positions from empties
|
||||
positions = []
|
||||
for empty in self.pi_empties:
|
||||
if empty and empty.name in bpy.data.objects:
|
||||
positions.append(tuple(empty.location))
|
||||
|
||||
if len(positions) < 2:
|
||||
return
|
||||
|
||||
# Setup blending for line drawing
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.depth_test_set("LESS_EQUAL")
|
||||
gpu.state.depth_mask_set(False)
|
||||
|
||||
# Build edges list
|
||||
edges = [[i, i + 1] for i in range(len(positions) - 1)]
|
||||
|
||||
# Draw lines
|
||||
self.draw_batch_3d("LINES", positions, self.COLOR_TANGENT_LINE, edges)
|
||||
|
||||
# Restore state
|
||||
gpu.state.blend_set("NONE")
|
||||
gpu.state.depth_test_set("NONE")
|
||||
gpu.state.depth_mask_set(True)
|
||||
|
||||
def draw_hud(self, context):
|
||||
"""Draw HUD text with edit mode instructions."""
|
||||
region = context.region
|
||||
if not region:
|
||||
return
|
||||
|
||||
font_id = 0
|
||||
font_size = tool.Blender.scale_font_size(14)
|
||||
blf.size(font_id, font_size)
|
||||
blf.enable(font_id, blf.SHADOW)
|
||||
blf.shadow(font_id, 6, 0, 0, 0, 1) # Black shadow for readability
|
||||
blf.color(font_id, *self.COLOR_HUD_TEXT)
|
||||
|
||||
# Position in top-left of viewport
|
||||
margin = 20
|
||||
line_height = 22
|
||||
y_pos = region.height - margin
|
||||
|
||||
# Count valid empties
|
||||
valid_count = sum(1 for e in self.pi_empties if e and e.name in bpy.data.objects)
|
||||
|
||||
# Instructions
|
||||
instructions = [
|
||||
"PI Edit Mode",
|
||||
f"PIs: {valid_count}",
|
||||
"",
|
||||
"G: Move selected PI",
|
||||
"ENTER: Apply changes",
|
||||
"ESC: Cancel",
|
||||
]
|
||||
|
||||
for i, line in enumerate(instructions):
|
||||
blf.position(font_id, margin, y_pos - (i * line_height), 0)
|
||||
blf.draw(font_id, line)
|
||||
|
||||
blf.disable(font_id, blf.SHADOW)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,215 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.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/>.
|
||||
|
||||
|
||||
"""Property groups for the alignment module"""
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
|
||||
def _on_radius_update(self, context):
|
||||
"""Callback when radius property changes.
|
||||
|
||||
This dynamically imports the operator module to call on_radius_changed,
|
||||
avoiding circular imports since prop.py is imported before operator.py.
|
||||
"""
|
||||
from . import operator as ops
|
||||
|
||||
ops.on_radius_changed(self, context)
|
||||
|
||||
|
||||
class AlignmentPI(PropertyGroup):
|
||||
"""Property group for a single PI (Point of Intersection)
|
||||
|
||||
In the PI method, alignments are defined by:
|
||||
- Endpoint PIs: Start (POB) and End (POE) points
|
||||
- Interior PIs: Points where tangents intersect, optionally with curves
|
||||
"""
|
||||
|
||||
# Coordinates stored as global easting/northing (map coordinates).
|
||||
# Coordinate flow: Blender coords -> xyz2enh() -> global E/N (stored here)
|
||||
# global E/N -> enh2xyz(to_blender=False) -> local IFC coords (for IfcOpenShell API)
|
||||
e: StringProperty(name="E", description="Easting (global map coordinates)", default="0.0")
|
||||
n: StringProperty(name="N", description="Northing (global map coordinates)", default="0.0")
|
||||
|
||||
# PI Type
|
||||
pi_type: EnumProperty(
|
||||
name="Type",
|
||||
description="Type of PI point",
|
||||
items=[
|
||||
("ENDPOINT", "Endpoint", "Start or end point (no curve)"),
|
||||
("TANGENT", "Tangent", "Pass-through point (no curve)"),
|
||||
("CURVE", "Curve", "Point of intersection with curve"),
|
||||
],
|
||||
default="TANGENT",
|
||||
)
|
||||
|
||||
# Curve parameters (only used when pi_type == "CURVE")
|
||||
radius: FloatProperty(
|
||||
name="Radius",
|
||||
description="Curve radius (0 = no curve, sharp angle)",
|
||||
default=0.0,
|
||||
min=0.0,
|
||||
precision=3,
|
||||
unit="LENGTH",
|
||||
update=_on_radius_update,
|
||||
)
|
||||
|
||||
# Computed/display values (updated by recalculate operator)
|
||||
length_to_next: FloatProperty(
|
||||
name="Length",
|
||||
description="Length of tangent to next PI",
|
||||
default=0.0,
|
||||
precision=3,
|
||||
unit="LENGTH",
|
||||
)
|
||||
|
||||
direction_to_next: FloatProperty(
|
||||
name="Direction",
|
||||
description="Bearing/direction to next PI (degrees)",
|
||||
default=0.0,
|
||||
precision=4,
|
||||
subtype="ANGLE",
|
||||
)
|
||||
|
||||
# Station at this PI (computed)
|
||||
station: FloatProperty(
|
||||
name="Station",
|
||||
description="Station value at this PI",
|
||||
default=0.0,
|
||||
precision=2,
|
||||
)
|
||||
|
||||
|
||||
class AlignmentDisplayRow(PropertyGroup):
|
||||
"""Property group for interleaved point/segment display in the table.
|
||||
|
||||
This creates the Civil 3D-style view where points and segments
|
||||
are shown on separate rows:
|
||||
Point 1 (End)
|
||||
Segment 1 (Tan)
|
||||
Point 2 (Tan)
|
||||
Segment 2 (Tan)
|
||||
...
|
||||
"""
|
||||
|
||||
# Row type discriminator
|
||||
row_type: EnumProperty(
|
||||
name="Row Type",
|
||||
items=[
|
||||
("POINT", "Point", "A PI point row"),
|
||||
("SEGMENT", "Segment", "A segment row between points"),
|
||||
],
|
||||
default="POINT",
|
||||
)
|
||||
|
||||
# Segment number (1, 2, 3...) - only for SEGMENT rows
|
||||
segment_number: IntProperty(name="Segment #", default=0)
|
||||
|
||||
# Point index in the pis collection - for both types
|
||||
# For POINT rows: the PI index
|
||||
# For SEGMENT rows: the starting PI index of this segment
|
||||
pi_index: IntProperty(name="PI Index", default=0)
|
||||
|
||||
# Display type string (End, Tan, Curve for points; Tan, Curve for segments)
|
||||
display_type: StringProperty(name="Type", default="")
|
||||
|
||||
# Point coordinates (only for POINT rows)
|
||||
e: StringProperty(name="E", default="0.0")
|
||||
n: StringProperty(name="N", default="0.0")
|
||||
|
||||
# Segment properties (only for SEGMENT rows)
|
||||
length: FloatProperty(name="Length", default=0.0, precision=2, unit="LENGTH")
|
||||
radius: FloatProperty(name="Radius", default=0.0, precision=2, unit="LENGTH")
|
||||
arc_length: FloatProperty(name="Arc Length", default=0.0, precision=2, unit="LENGTH")
|
||||
|
||||
|
||||
class CivilAlignmentProperties(PropertyGroup):
|
||||
"""Properties for the alignment module"""
|
||||
|
||||
# Active alignment selection
|
||||
active_alignment_id: IntProperty(
|
||||
name="Active Alignment ID",
|
||||
description="IFC entity ID of the active alignment",
|
||||
default=0,
|
||||
)
|
||||
|
||||
active_alignment_name: StringProperty(
|
||||
name="Active Alignment",
|
||||
description="Name of the currently active alignment",
|
||||
default="",
|
||||
)
|
||||
|
||||
# New alignment creation properties
|
||||
new_alignment_name: StringProperty(
|
||||
name="Name",
|
||||
description="Name for new alignment",
|
||||
default="Alignment 1",
|
||||
)
|
||||
|
||||
start_station: FloatProperty(
|
||||
name="Start Station",
|
||||
description="Starting station value (e.g., 10000 for 100+00)",
|
||||
default=10000.0,
|
||||
min=0.0,
|
||||
)
|
||||
|
||||
# PI collection for PI method creation
|
||||
pis: CollectionProperty(type=AlignmentPI)
|
||||
active_pi_index: IntProperty(name="Active PI", default=0)
|
||||
|
||||
# Combined point/segment display rows (for Civil 3D-style table)
|
||||
display_rows: CollectionProperty(type=AlignmentDisplayRow)
|
||||
active_display_row_index: IntProperty(name="Active Display Row", default=0)
|
||||
|
||||
# PI Edit Mode state (for moving PIs with G key)
|
||||
is_pi_edit_mode: BoolProperty(
|
||||
name="PI Edit Mode Active",
|
||||
description="Whether PI edit mode is currently active",
|
||||
default=False,
|
||||
)
|
||||
|
||||
pi_edit_alignment_id: IntProperty(
|
||||
name="Editing Alignment ID",
|
||||
description="IFC ID of alignment being edited in PI edit mode",
|
||||
default=0,
|
||||
)
|
||||
|
||||
# Display options
|
||||
show_station_labels: BoolProperty(
|
||||
name="Show Station Labels",
|
||||
description="Show station labels along alignment",
|
||||
default=True,
|
||||
)
|
||||
|
||||
station_interval: FloatProperty(
|
||||
name="Station Interval",
|
||||
description="Interval between station markers",
|
||||
default=100.0,
|
||||
min=1.0,
|
||||
unit="LENGTH",
|
||||
)
|
||||
@@ -0,0 +1,269 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025, 2026 Michael Yoder <myoder@desertspringscivil.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/>.
|
||||
|
||||
|
||||
"""UI panels for the alignment module
|
||||
|
||||
All panels appear in the Properties sidebar under the CIVIL tab,
|
||||
nested under BIM_PT_tab_horizontal_alignment.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
|
||||
|
||||
def is_ifc4x3():
|
||||
"""Check if the current IFC file is IFC4X3 schema"""
|
||||
return tool.Ifc.get_schema() == "IFC4X3"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UILists
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class CIVIL_UL_alignment_pis(UIList):
|
||||
"""UIList for displaying interleaved points and segments (Civil 3D style)
|
||||
|
||||
Row types:
|
||||
- POINT rows: End (endpoint), Mid (interior PI without curve)
|
||||
- SEGMENT rows: Tan (tangent line), Curve (circular arc)
|
||||
|
||||
When a Mid point has radius > 0, it becomes a Curve segment row.
|
||||
"""
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
row = layout.row(align=True)
|
||||
|
||||
if item.row_type == "POINT":
|
||||
# Point row: No., Type, X, Y, Length, Radius
|
||||
row.label(text="") # No segment number for points
|
||||
|
||||
# Type with point/dot icon
|
||||
# "End" = endpoint (POB/POE), "Mid" = interior PI point
|
||||
row.label(text=item.display_type, icon="DOT")
|
||||
|
||||
# X, Y coordinates - get actual PI for editing
|
||||
pi = data.pis[item.pi_index] if item.pi_index < len(data.pis) else None
|
||||
if pi:
|
||||
sub = row.row(align=True)
|
||||
sub.prop(pi, "e", text="")
|
||||
sub.prop(pi, "n", text="")
|
||||
else:
|
||||
row.label(text=f"{float(item.e):.2f}")
|
||||
row.label(text=f"{float(item.n):.2f}")
|
||||
|
||||
# Length column - empty for point rows
|
||||
row.label(text="")
|
||||
|
||||
# Radius column - editable for Mid points (where curves can be added)
|
||||
if item.display_type == "Mid" and pi:
|
||||
row.prop(pi, "radius", text="")
|
||||
else:
|
||||
row.label(text="")
|
||||
|
||||
elif item.row_type == "SEGMENT":
|
||||
if item.display_type == "Curve":
|
||||
# Curve segment row: No., Type (arc icon), X, Y, Arc Length, Radius
|
||||
row.label(text=f"{item.segment_number}")
|
||||
row.label(text="Curve", icon="SPHERECURVE")
|
||||
|
||||
# Show PI coordinates on curve row
|
||||
row.label(text=f"{float(item.e):.2f}")
|
||||
row.label(text=f"{float(item.n):.2f}")
|
||||
|
||||
# Arc length
|
||||
row.label(text=f"{item.arc_length:.2f}")
|
||||
|
||||
# Radius - editable so user can modify or delete curve (set to 0)
|
||||
pi = data.pis[item.pi_index] if item.pi_index < len(data.pis) else None
|
||||
if pi:
|
||||
row.prop(pi, "radius", text="")
|
||||
else:
|
||||
row.label(text=f"{item.radius:.2f}")
|
||||
else:
|
||||
# Tangent segment row: No., Type (line icon), -, -, Length, -
|
||||
row.label(text=f"{item.segment_number}")
|
||||
row.label(text="Tan", icon="IPO_LINEAR")
|
||||
|
||||
# No X, Y for tangent segments
|
||||
row.label(text="")
|
||||
row.label(text="")
|
||||
|
||||
# Length
|
||||
row.label(text=f"{item.length:.2f}")
|
||||
|
||||
# No radius for tangent segments
|
||||
row.label(text="-")
|
||||
|
||||
elif self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", icon="DECORATE")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Creation Sub-Panel
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class CIVIL_PT_alignment_creation(Panel):
|
||||
"""Sub-panel for alignment creation tools"""
|
||||
|
||||
bl_label = "Creation"
|
||||
bl_idname = "CIVIL_PT_alignment_creation"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_horizontal_alignment"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.should_show_panel(context, "CIVIL", cls.bl_idname) and is_ifc4x3()
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.CivilAlignmentProperties
|
||||
|
||||
# New alignment properties
|
||||
box = layout.box()
|
||||
box.label(text="New Alignment:", icon="ADD")
|
||||
box.prop(props, "new_alignment_name")
|
||||
box.prop(props, "start_station")
|
||||
|
||||
# Creation operators
|
||||
col = layout.column(align=True)
|
||||
col.operator("civil.create_alignment_by_pi", icon="CURVE_DATA")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PI Editor Sub-Panel
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class CIVIL_PT_pi_editor(Panel):
|
||||
"""Sub-panel for PI point table editor (Civil 3D style grid view)"""
|
||||
|
||||
bl_label = "PI Editor"
|
||||
bl_idname = "CIVIL_PT_pi_editor"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_horizontal_alignment"
|
||||
bl_options = set() # Open by default
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.should_show_panel(context, "CIVIL", cls.bl_idname) and is_ifc4x3()
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.CivilAlignmentProperties
|
||||
|
||||
# PI Edit Mode indicator
|
||||
if props.is_pi_edit_mode:
|
||||
box = layout.box()
|
||||
box.alert = True
|
||||
box.label(text="PI Edit Mode Active", icon="EDITMODE_HLT")
|
||||
col = box.column(align=True)
|
||||
col.label(text="Move PIs with G key")
|
||||
col.label(text="Press Enter to apply")
|
||||
col.label(text="Press Escape to cancel")
|
||||
layout.separator()
|
||||
return # Don't show normal UI while in edit mode
|
||||
|
||||
# Edit existing alignment button
|
||||
if props.active_alignment_id != 0:
|
||||
box = layout.box()
|
||||
box.label(text="Edit Alignment:", icon="EDITMODE_HLT")
|
||||
box.operator("civil.enter_pi_edit_mode", icon="PIVOT_CURSOR", text="Edit PIs (G key)")
|
||||
layout.separator()
|
||||
|
||||
# Header row with column labels
|
||||
header = layout.row(align=True)
|
||||
header.label(text="No.")
|
||||
header.label(text="Type")
|
||||
header.label(text="E")
|
||||
header.label(text="N")
|
||||
header.label(text="Length")
|
||||
header.label(text="Radius")
|
||||
|
||||
# Combined point/segment list (interleaved view)
|
||||
row = layout.row()
|
||||
row.template_list(
|
||||
"CIVIL_UL_alignment_pis",
|
||||
"",
|
||||
props,
|
||||
"display_rows",
|
||||
props,
|
||||
"active_display_row_index",
|
||||
rows=8,
|
||||
)
|
||||
|
||||
# Side buttons for list management
|
||||
col = row.column(align=True)
|
||||
col.operator("civil.add_pi", icon="ADD", text="")
|
||||
col.operator("civil.remove_pi", icon="REMOVE", text="")
|
||||
col.separator()
|
||||
col.operator("civil.pick_pi_from_viewport", icon="EYEDROPPER", text="")
|
||||
|
||||
# Bottom actions
|
||||
layout.separator()
|
||||
row = layout.row(align=True)
|
||||
row.operator("civil.recalculate_pis", icon="FILE_REFRESH", text="Recalculate")
|
||||
row.operator("civil.clear_pis", icon="TRASH", text="Clear All")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Stationing Sub-Panel
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class CIVIL_PT_alignment_stationing(Panel):
|
||||
"""Sub-panel for stationing and referents"""
|
||||
|
||||
bl_label = "Stationing"
|
||||
bl_idname = "CIVIL_PT_alignment_stationing"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_horizontal_alignment"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.should_show_panel(context, "CIVIL", cls.bl_idname) and is_ifc4x3()
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.CivilAlignmentProperties
|
||||
|
||||
# Station display options
|
||||
box = layout.box()
|
||||
box.label(text="Display:", icon="HIDE_OFF")
|
||||
box.prop(props, "show_station_labels")
|
||||
box.prop(props, "station_interval")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Stationing operators
|
||||
col = layout.column(align=True)
|
||||
col.operator("civil.add_stationing_referent", icon="EMPTY_AXIS")
|
||||
col.operator("civil.name_segments", icon="FONT_DATA")
|
||||
@@ -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:
|
||||
|
||||
@@ -23,12 +23,16 @@ from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.prop import Attribute
|
||||
from bonsai.bim.prop import Attribute, StrProperty
|
||||
|
||||
|
||||
class BIMAttributeProperties(PropertyGroup):
|
||||
@@ -41,7 +45,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 +64,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 +80,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:
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
import bpy.props
|
||||
import bpy.types
|
||||
|
||||
|
||||
class AuginProperties(bpy.types.PropertyGroup):
|
||||
@@ -28,11 +27,3 @@ class AuginProperties(bpy.types.PropertyGroup):
|
||||
project_name: bpy.props.StringProperty(name="Project Name")
|
||||
project_filename: bpy.props.StringProperty(name="IFC Filename")
|
||||
is_success: bpy.props.BoolProperty(name="Is Successful Upload", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
username: str
|
||||
password: str
|
||||
token: str
|
||||
project_name: str
|
||||
project_filename: str
|
||||
is_success: bool
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
import bcf
|
||||
import bcf.bcfxml
|
||||
import bcf.v2.bcfxml
|
||||
import bpy
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
@@ -16,18 +16,22 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
import webbrowser
|
||||
from math import atan, degrees, radians, tan
|
||||
from math import atan, cos, degrees, radians, sin, tan
|
||||
from pathlib import Path
|
||||
|
||||
import bcf
|
||||
import bcf.agnostic.topic
|
||||
import bcf.agnostic.visinfo
|
||||
import bcf.bcfxml
|
||||
import bcf.v2.bcfxml
|
||||
import bcf.v2.model
|
||||
import bcf.v2.topic
|
||||
import bcf.v2.visinfo
|
||||
import bcf.v3
|
||||
import bcf.v3.bcfxml
|
||||
import bcf.v3.document
|
||||
import bcf.v3.model
|
||||
@@ -39,10 +43,11 @@ import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.unit
|
||||
import numpy as np
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from mathutils import Matrix, Vector
|
||||
from mathutils import Euler, Matrix, Vector, geometry
|
||||
from xsdata.models.datatype import XmlDateTime
|
||||
|
||||
import bonsai.bim.module.bcf.bcfstore as bcfstore
|
||||
import bonsai.bim.module.bcf.prop as bcf_prop
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
@@ -1253,8 +1258,8 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
else:
|
||||
obj.data.show_background_images = False
|
||||
|
||||
assert (space := tool.Blender.get_view3d_space())
|
||||
space.region_3d.view_perspective = "CAMERA"
|
||||
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
|
||||
area.spaces[0].region_3d.view_perspective = "CAMERA"
|
||||
|
||||
if self.file:
|
||||
self.set_viewpoint_components(viewpoint, context)
|
||||
|
||||
@@ -24,6 +24,8 @@ from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
@@ -230,7 +232,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()
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -20,6 +20,7 @@ import bmesh
|
||||
import gpu
|
||||
from bpy.types import SpaceView3D
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
@@ -56,6 +57,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 +110,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)
|
||||
|
||||
@@ -27,18 +27,20 @@ import ifcopenshell.api
|
||||
import ifcopenshell.api.boundary
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.unit
|
||||
import mathutils
|
||||
import numpy as np
|
||||
import shapely
|
||||
import shapely.ops
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.bim.import_ifc as import_ifc
|
||||
import bonsai.core
|
||||
import bonsai.core.geometry
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
@@ -377,8 +379,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 +394,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 +413,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"}
|
||||
@@ -708,7 +704,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
shapes[shape.id] = {
|
||||
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
|
||||
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
|
||||
|
||||
@@ -21,8 +21,13 @@ from typing import TYPE_CHECKING, Union
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
@@ -51,43 +56,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 +69,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):
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from bpy.types import Panel
|
||||
import bpy
|
||||
from bpy.types import Panel, UIList
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.boundary.data import SpaceBoundariesData
|
||||
@@ -77,10 +78,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 +85,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 +111,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)
|
||||
|
||||
@@ -63,7 +63,8 @@ class BrickschemaData:
|
||||
if namespace == "https://brickschema.org/schema/Brick":
|
||||
return []
|
||||
results = []
|
||||
query = BrickStore.graph.query("""
|
||||
query = BrickStore.graph.query(
|
||||
"""
|
||||
PREFIX brick: <https://brickschema.org/schema/Brick#>
|
||||
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
||||
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
||||
@@ -80,7 +81,10 @@ class BrickschemaData:
|
||||
}
|
||||
}
|
||||
GROUP BY ?object
|
||||
""".replace("{uri}", uri))
|
||||
""".replace(
|
||||
"{uri}", uri
|
||||
)
|
||||
)
|
||||
for row in query:
|
||||
predicate_uri = row.get("predicate")
|
||||
predicate_name = predicate_uri.toPython().split("#")[-1]
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import os
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
|
||||
import bonsai.bim.handler
|
||||
|
||||
@@ -23,7 +23,10 @@ from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -31,7 +34,7 @@ from bpy.types import PropertyGroup
|
||||
import bonsai.core.brick as core
|
||||
import bonsai.tool.brick as tool
|
||||
from bonsai.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData
|
||||
from bonsai.bim.prop import StrProperty
|
||||
from bonsai.bim.prop import Attribute, StrProperty
|
||||
from bonsai.tool.brick import BrickStore
|
||||
|
||||
|
||||
@@ -46,26 +49,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":
|
||||
|
||||
@@ -16,18 +16,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
from bpy.types import Panel, UIList
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.brick.prop import Brick
|
||||
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bonsai.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData
|
||||
from bonsai.tool.brick import BrickStore
|
||||
@@ -283,9 +274,7 @@ class BIM_PT_brickschema_viewport(Panel):
|
||||
class BIM_UL_bricks(UIList):
|
||||
split_screen = False
|
||||
|
||||
def draw_item(
|
||||
self, context, layout: bpy.types.UILayout, data, item: Brick, icon, active_data, active_propname
|
||||
) -> None:
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
split = layout.split(factor=0.85, align=True)
|
||||
row = split.row()
|
||||
|
||||
@@ -16,8 +16,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.classification
|
||||
import ifcopenshell.util.date
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def refresh():
|
||||
|
||||
@@ -19,6 +19,7 @@ import textwrap
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
import bsdd
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
@@ -23,7 +23,10 @@ from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -31,7 +34,7 @@ from bpy.types import PropertyGroup
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.bsdd.data import BSDDData
|
||||
from bonsai.bim.module.classification.data import ClassificationsData
|
||||
from bonsai.bim.prop import Attribute
|
||||
from bonsai.bim.prop import Attribute, StrProperty
|
||||
|
||||
|
||||
def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
@@ -160,7 +163,7 @@ class BIMBSDDProperties(PropertyGroup):
|
||||
default=False,
|
||||
)
|
||||
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
active_dictionary: str
|
||||
active_dictionary: str
|
||||
@@ -179,7 +182,7 @@ class BIMBSDDProperties(PropertyGroup):
|
||||
should_filter_ifc_class: bool
|
||||
use_only_ifc_properties: bool
|
||||
classification_psets: bpy.types.bpy_prop_collection_idprop[BSDDPset]
|
||||
|
||||
|
||||
@property
|
||||
def active_class(self) -> Union[BSDDClassification, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.classes, self.active_class_index)
|
||||
|
||||
@@ -24,6 +24,7 @@ import bpy
|
||||
from bpy.types import Panel, UIList
|
||||
|
||||
import bonsai.tool as tool
|
||||
import bsdd
|
||||
from bonsai.bim.module.bsdd.data import BSDDData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -83,7 +84,6 @@ class BIM_PT_bsdd(Panel):
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_bsdd_dictionaries")
|
||||
|
||||
|
||||
class BIM_UL_bsdd_dictionaries(UIList):
|
||||
def draw_item(
|
||||
self,
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import math
|
||||
from math import pi, sqrt
|
||||
from math import cos, pi, radians, sin, sqrt
|
||||
from typing import Union
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import bpy_extras
|
||||
import ifcopenshell.util.unit
|
||||
import mathutils
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
@@ -37,7 +39,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 +84,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 +345,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):
|
||||
@@ -413,11 +405,6 @@ class CadOffset(bpy.types.Operator):
|
||||
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 = []
|
||||
@@ -530,15 +517,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)
|
||||
|
||||
@@ -547,14 +531,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()
|
||||
|
||||
@@ -22,16 +22,13 @@ from typing import TYPE_CHECKING
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
|
||||
|
||||
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 +39,6 @@ class BIMCadProperties(PropertyGroup):
|
||||
resolution: int
|
||||
radius: float
|
||||
distance: float
|
||||
copy: bool
|
||||
x: float
|
||||
y: float
|
||||
gable_roof_edge_angle: float
|
||||
|
||||
@@ -20,10 +20,12 @@ import os
|
||||
from functools import partial
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import WorkSpaceTool
|
||||
|
||||
import bonsai.bim.module.type.prop as type_prop
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.model.data import RailingData, RoofData
|
||||
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
|
||||
|
||||
def load_custom_icons():
|
||||
@@ -106,37 +108,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 +134,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 +170,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 +220,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 +255,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,6 +18,9 @@
|
||||
|
||||
import json
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.element
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
|
||||
@@ -17,18 +17,45 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blf
|
||||
import bmesh
|
||||
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()
|
||||
|
||||
@@ -18,13 +18,16 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from math import radians
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import numpy as np
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
@@ -201,10 +204,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"},
|
||||
)
|
||||
|
||||
@@ -451,7 +460,9 @@ class HideClash(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
ClashDecorator.uninstall()
|
||||
tool.Blender.update_all_viewports(context)
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from bpy.props import (
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -33,16 +34,16 @@ from ifcopenshell.geom.main import CLASH_TYPE_ITEMS, ClashType
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.prop import BIMFilterGroup, StrProperty
|
||||
from bonsai.bim.prop import Attribute, 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 +63,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),
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.classification
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.classification
|
||||
|
||||
@@ -23,7 +23,10 @@ from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
@@ -25,6 +25,7 @@ import ifcopenshell.util.classification
|
||||
from bpy.types import Panel, UIList
|
||||
|
||||
import bonsai.bim.helper
|
||||
import bonsai.bim.module.classification.prop as classification_prop
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.classification.data import (
|
||||
ClassificationsData,
|
||||
|
||||
@@ -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")],
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user