mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10781398d4 | |||
| da470c5135 | |||
| 214cd44f8e | |||
| 4bff2fa554 | |||
| 9d78df392d | |||
| 86bef0a254 | |||
| 05bf59d360 | |||
| 17eaef778a | |||
| f46be80193 | |||
| be05d771a2 | |||
| c214d255c9 | |||
| 0d8ba71384 | |||
| 1c26ee86c9 | |||
| 0ed96d32dd | |||
| f96526195d | |||
| fd29481d65 | |||
| 24b48497f0 | |||
| 2b9822f141 | |||
| 3d4db13fc1 | |||
| 31b571322b | |||
| cef5d41b54 | |||
| d7b2358d58 | |||
| cafe5aa7f7 | |||
| de34e73451 | |||
| 5721a8b602 | |||
| f820214500 | |||
| dae913e06a | |||
| 1a849395c2 | |||
| 611273a20a | |||
| db68195310 | |||
| 29079e8cba | |||
| 6bf4259298 | |||
| 7cd40bf8cb | |||
| 8b8f78095d | |||
| 23ba9e4db0 | |||
| 1aec991f08 | |||
| bddf9b85f8 | |||
| f679c63a18 | |||
| e6cc0e7813 | |||
| cf2acfc649 | |||
| 60ebb99fda | |||
| ab96add772 | |||
| d8de623086 | |||
| a3f2e061fb | |||
| f51a4673db | |||
| 75b8d4f218 | |||
| b8136d4762 | |||
| ecde429d36 | |||
| 1771b34449 | |||
| 41469acbc8 | |||
| 547b22199f | |||
| 94c15213f6 | |||
| fca258fb07 | |||
| bcfad8d96d | |||
| c026dd3b6e | |||
| d0f20371bd | |||
| 7b6e82a9cc | |||
| 286c69429d | |||
| 9c22dc6013 | |||
| 0acbd5ffad | |||
| 77f0c43314 | |||
| c1a9708504 | |||
| bcf6f6197d | |||
| 1778656bd5 | |||
| 54f450129c | |||
| 9fad1569c7 | |||
| 722c374fa6 | |||
| 91b6c3e256 | |||
| ea3f71b4e0 | |||
| 1a8b17e235 | |||
| 2bad861122 | |||
| 6c1fb3b01a | |||
| f5be64af6c | |||
| b043dd4d04 | |||
| ffd2466321 | |||
| e9241fd812 | |||
| 48d45451ac | |||
| 3b7cf6e865 | |||
| 6038373ee5 | |||
| 3d7de87b46 | |||
| cb113ae8da | |||
| 26280d24fe | |||
| 30551cb288 | |||
| 3bf0edeca2 | |||
| 3590b08e68 | |||
| fd902d88fb | |||
| aa5f5120e0 | |||
| 81986bcbfb | |||
| ec6c268cdb | |||
| 64003fd5ef | |||
| 58d07bace4 | |||
| 3b718bc58d | |||
| 18c035ea77 | |||
| f2e2e324b1 | |||
| 069dbbd8c2 | |||
| 3385872e8b | |||
| f0b27a0910 | |||
| 888158570a | |||
| c03156b5cd | |||
| 05bf2b82d2 | |||
| 7bdc1b6a75 | |||
| 03de69814a | |||
| 4dc6a0f2bc | |||
| c33509364c | |||
| 47f058341b | |||
| a51b2c587c | |||
| 845a13ba83 | |||
| 530841967e | |||
| c36e7badae | |||
| 515fe8d2ef | |||
| b8d3d1d105 | |||
| e95da857d6 | |||
| d587d1ac11 | |||
| ba36dc82ff | |||
| 025fb769e2 | |||
| bf75a19640 | |||
| 4473dbd138 | |||
| 6117417b89 | |||
| 0398584c69 | |||
| 0c69f85d5e | |||
| 7e987be00f | |||
| 35e3d9c42e | |||
| 63a8639353 | |||
| bd15ba4aa3 | |||
| f583d1ecc1 | |||
| 5bfab569ba | |||
| fb16e91249 | |||
| 1c456c3cb2 | |||
| a8d28fb469 | |||
| 62bb6cdf33 | |||
| cf153981ce | |||
| b38316336c | |||
| 021e6b9ef5 | |||
| 1999d93f9a | |||
| 8c9e89ace8 | |||
| 868bb5c39e | |||
| c3a87c8f9c | |||
| c30d24c4d9 | |||
| 2edd1a5044 | |||
| 08dfcea47c | |||
| 9c8f25739c | |||
| 256e4d2191 | |||
| b14da14614 | |||
| 273ecfe8e4 | |||
| 6e0f24105e | |||
| 25af50a092 | |||
| 4b5a50a831 | |||
| bd77175c66 | |||
| 2b1d99a8b1 | |||
| 82adf4d18c |
@@ -53,7 +53,7 @@ jobs:
|
||||
python ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
with:
|
||||
key: mac-${{ matrix.arch }}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
python ../IfcOpenShell/nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
python3 ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
python3 ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
with:
|
||||
key: win-${{ matrix.arch }}
|
||||
# Windows ccache needs ~1GB
|
||||
|
||||
@@ -7,6 +7,9 @@ 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
|
||||
@@ -14,12 +17,12 @@ jobs:
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: ${{ env.MIN_IOS_PY_VERSION }}
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -35,8 +38,8 @@ jobs:
|
||||
ERROR=0
|
||||
# Using 2 Python versions - one minimum required for IfcOpenShell
|
||||
# and other that's used by Blender currently.
|
||||
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
||||
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
|
||||
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
|
||||
exit $ERROR
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
@@ -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.0/blender-5.0.1-linux-x64.tar.xz
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.0/blender-5.1.0-linux-x64.tar.xz
|
||||
tar -xf blender.tar.xz
|
||||
|
||||
# Setup Blender.
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v3 # 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@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v3 # 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.20
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
|
||||
-
|
||||
name: Build ifcopenshell
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
lfs: true
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v8.0.0
|
||||
uses: actions/download-artifact@v8.0.1
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
pyver: [py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
pyver: [py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
libhdf5-dev libcgal-dev libeigen3-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
||||
uses: hendrikmuhs/ccache-action@v1.2.22
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
@@ -254,6 +254,7 @@ jobs:
|
||||
cd ../ifcpatch && make test || ERROR=1
|
||||
pip install -e ../ifctester --no-deps
|
||||
cd ../ifctester && make test || ERROR=1
|
||||
make build-ids-docs || ERROR=1
|
||||
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
|
||||
cd ../ifcopenshell-python
|
||||
pip install mathutils
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@v6
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
+16
-3
@@ -5,6 +5,8 @@
|
||||
/_installed-vs*-x*/
|
||||
/build/
|
||||
/src/examples/build/
|
||||
# ifctester docs output
|
||||
/src/ifctester/test/build/
|
||||
|
||||
# output directories
|
||||
/cmake/out/
|
||||
@@ -12,6 +14,7 @@
|
||||
/src/ifcmax/out/
|
||||
/src/ifcwrap/out/
|
||||
/src/qtviewer/out/
|
||||
/src/ifctester/webapp/public/pyodide/
|
||||
|
||||
/win/BuildDepsCache*.txt
|
||||
|
||||
@@ -80,10 +83,14 @@ src/ifcopenshell-python/test/build
|
||||
# bonsai i18n
|
||||
src/bonsai/bonsai/translations.py
|
||||
|
||||
# bonsai test temp files
|
||||
# bonsai external dependencies (cloned for just ty checks)
|
||||
src/bonsai/external_dependencies/
|
||||
|
||||
# bonsai test temp/cache files
|
||||
src/bonsai/test/files/temp
|
||||
src/bonsai/test/files/basic.ifc.cache.blend
|
||||
src/bonsai/test/files/basic.ifc.cache.sqlite
|
||||
src/bonsai/test/files/*.cache.blend
|
||||
src/bonsai/test/files/*.cache.json
|
||||
src/bonsai/test/files/*.cache.sqlite
|
||||
|
||||
# bonsai data
|
||||
src/bonsai/bonsai/bim/data/build/
|
||||
@@ -115,3 +122,9 @@ 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*
|
||||
|
||||
@@ -13,6 +13,7 @@ import hashlib
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
from typing import NoReturn
|
||||
from urllib import request
|
||||
|
||||
@@ -20,7 +21,7 @@ from github import Github
|
||||
|
||||
|
||||
def get_repo_tag_names() -> list[str]:
|
||||
git_return = os.popen("git tag -l").read()
|
||||
git_return = subprocess.check_output("git tag -l", text=True)
|
||||
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
|
||||
@@ -78,6 +79,10 @@ 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"
|
||||
@@ -97,7 +102,7 @@ should_release = False
|
||||
target_release_tag = ""
|
||||
TARGET_OS = "windows-x64"
|
||||
|
||||
git_status = os.popen("git status").read()
|
||||
git_status = subprocess.check_output("git status", text=True)
|
||||
print(git_status)
|
||||
|
||||
for tag_name in get_repo_tag_names():
|
||||
@@ -147,7 +152,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)
|
||||
os.popen(f"wget {url_blenderbim_py3x_win_zip} --no-verbose").read()
|
||||
subprocess.check_call(f"wget {url_blenderbim_py3x_win_zip} --no-verbose")
|
||||
|
||||
# sha256sum_blenderbim_py310_win_zip
|
||||
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
|
||||
@@ -201,13 +206,13 @@ print("[INFO] inserting dynamic chocolatey package parameters successful")
|
||||
print("\n_____ build choco.exe with mono")
|
||||
|
||||
choco_version = "1.1.0"
|
||||
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()
|
||||
run(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet")
|
||||
run(f"tar -xzf {choco_version}.tar.gz")
|
||||
print("choco tar unpack successful")
|
||||
os.chdir("choco-1.1.0")
|
||||
os.popen("./build.sh").read()
|
||||
run("./build.sh")
|
||||
|
||||
os.popen("cp -r build_output/chocolatey /opt/chocolatey").read()
|
||||
run("cp -r build_output/chocolatey /opt/chocolatey")
|
||||
os.chdir(BLENDERBIM_DIR)
|
||||
|
||||
if pathlib.Path("/opt/chocolatey/choco.exe").exists():
|
||||
@@ -215,11 +220,15 @@ if pathlib.Path("/opt/chocolatey/choco.exe").exists():
|
||||
|
||||
print("\n_____ build choco pack")
|
||||
|
||||
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()
|
||||
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'
|
||||
)
|
||||
|
||||
print("\n_____ build choco push")
|
||||
os.popen('mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose').read()
|
||||
run(
|
||||
'mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose'
|
||||
)
|
||||
|
||||
print(f"choco push of version: {target_release_tag} successful!")
|
||||
print(f"it took: {datetime.datetime.now() - start}")
|
||||
|
||||
@@ -41,6 +41,9 @@ def pack_dependencies(install_dir: Path) -> None:
|
||||
if not dependency_path.is_dir():
|
||||
continue
|
||||
dependency_name = dependency_path.name
|
||||
# Skip ifcopenshell - it's a build output, not a dependency to reuse across builds.
|
||||
if dependency_name == "ifcopenshell":
|
||||
continue
|
||||
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
|
||||
if tar_path.exists():
|
||||
print(f"Skipping existing cache: '{tar_path}'")
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build an ifcopenshell WASM wheel using Pyodide build system.
|
||||
|
||||
Usage:
|
||||
python make_wheel.py # Show this help
|
||||
python make_wheel.py --build # Build wheel
|
||||
python make_wheel.py --clean # Clean build artifacts and exit
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import platform
|
||||
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,
|
||||
venv: Path | None = None,
|
||||
) -> None:
|
||||
if not venv:
|
||||
print(f"$ {' '.join(cmd)}")
|
||||
subprocess.check_call(cmd, cwd=cwd)
|
||||
return
|
||||
|
||||
if platform.system() == "Windows":
|
||||
activate = venv / ".venv" / "Scripts" / "activate.bat"
|
||||
cmd_str = f'"{activate}" && {" ".join(cmd)}'
|
||||
else:
|
||||
activate = venv / ".venv" / "bin" / "activate"
|
||||
cmd_str = f'source "{activate}" && {" ".join(cmd)}'
|
||||
print(f"$ {cmd_str}")
|
||||
subprocess.check_call(cmd_str, shell=True, 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 / ".venv",
|
||||
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("Creating venv...")
|
||||
Tools.run(["uv", "venv", "--clear", "--python", "3.13"], cwd=PYODIDE_DIR)
|
||||
|
||||
print("Installing pyodide-build...")
|
||||
if args.dev:
|
||||
Tools.run(["uv", "pip", "install", "-e", str(PYODIDE_BUILD)], cwd=PYODIDE_DIR)
|
||||
else:
|
||||
Tools.run(["uv", "pip", "install", "pyodide-build"], cwd=PYODIDE_DIR)
|
||||
|
||||
print("Installing setuptools...")
|
||||
Tools.run(["uv", "pip", "install", "setuptools"], cwd=PYODIDE_DIR)
|
||||
|
||||
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.
|
||||
Tools.run(
|
||||
["pyodide", "build", "--no-isolation", f"-C--build-option=--plat-name={WHEEL_PLATFORM_TAG}"],
|
||||
cwd=PYODIDE_DIR,
|
||||
venv=PYODIDE_DIR,
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"\n✓ Done! ({elapsed:.1f}s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+37
-1
@@ -2,12 +2,16 @@
|
||||
# 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
|
||||
|
||||
REPO_FOLDER = Path(__file__).parent
|
||||
# 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
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
@@ -24,6 +28,37 @@ def get_dependencies() -> list[str]:
|
||||
dependencies = pyproject_data["project"]["dependencies"]
|
||||
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",
|
||||
@@ -44,4 +79,5 @@ setup(
|
||||
},
|
||||
# Has to provide extension to get the correct wheel suffix.
|
||||
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
|
||||
cmdclass={'build_ext': UnixBuildExt},
|
||||
)
|
||||
|
||||
+176
-2
@@ -3,9 +3,9 @@ name = "IfcOpenShell"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"black==26.3.1",
|
||||
"ruff==0.15.5",
|
||||
"ruff==0.15.8",
|
||||
"poethepoet",
|
||||
"gersemi==0.26.0",
|
||||
"gersemi==0.26.1",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
@@ -78,6 +78,139 @@ 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"
|
||||
byte-string-type-annotation = "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"
|
||||
fstring-type-annotation = "error"
|
||||
ignore-comment-unknown-rule = "error"
|
||||
implicit-concatenated-string-type-annotation = "error"
|
||||
inconsistent-mro = "error"
|
||||
ineffective-final = "error"
|
||||
instance-layout-conflict = "error"
|
||||
invalid-dataclass = "error"
|
||||
invalid-dataclass-override = "error"
|
||||
invalid-enum-member-annotation = "error"
|
||||
invalid-explicit-override = "error"
|
||||
invalid-frozen-dataclass-subclass = "error"
|
||||
invalid-generic-class = "error"
|
||||
invalid-generic-enum = "error"
|
||||
invalid-ignore-comment = "error"
|
||||
invalid-legacy-positional-parameter = "error"
|
||||
invalid-legacy-type-variable = "error"
|
||||
invalid-named-tuple = "error"
|
||||
invalid-newtype = "error"
|
||||
invalid-overload = "error"
|
||||
invalid-paramspec = "error"
|
||||
invalid-protocol = "error"
|
||||
invalid-syntax-in-forward-annotation = "error"
|
||||
invalid-total-ordering = "error"
|
||||
invalid-type-alias-type = "error"
|
||||
invalid-type-checking-constant = "error"
|
||||
invalid-type-guard-definition = "error"
|
||||
invalid-type-variable-bound = "error"
|
||||
invalid-type-variable-constraints = "error"
|
||||
invalid-typed-dict-header = "error"
|
||||
invalid-typed-dict-statement = "error"
|
||||
override-of-final-method = "error"
|
||||
override-of-final-variable = "error"
|
||||
possibly-missing-import = "error"
|
||||
possibly-missing-submodule = "error"
|
||||
# Has false positives due to ty walrus operator bug.
|
||||
# possibly-unresolved-reference = "error"
|
||||
raw-string-type-annotation = "error"
|
||||
redundant-final-classvar = "error"
|
||||
shadowed-type-variable = "error"
|
||||
subclass-of-final-class = "error"
|
||||
super-call-in-named-tuple-method = "error"
|
||||
unavailable-implicit-super-arguments = "error"
|
||||
unbound-type-variable = "error"
|
||||
undefined-reveal = "error"
|
||||
unresolved-global = "error"
|
||||
unresolved-import = "error"
|
||||
unresolved-reference = "error"
|
||||
unused-ignore-comment = "error"
|
||||
unused-type-ignore-comment = "error"
|
||||
useless-overload-body = "error"
|
||||
|
||||
# Non-structural rules:
|
||||
deprecated = "error"
|
||||
zero-stepsize-in-slice = "error"
|
||||
possibly-missing-implicit-call = "error"
|
||||
unused-awaitable = "error"
|
||||
|
||||
# Function argument rules:
|
||||
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
|
||||
# call-non-callable = "error"
|
||||
conflicting-argument-forms = "error"
|
||||
# Too many false positives.
|
||||
# invalid-argument-type = "error"
|
||||
missing-argument = "error"
|
||||
parameter-already-assigned = "error"
|
||||
positional-only-parameter-as-kwarg = "error"
|
||||
too-many-positional-arguments = "error"
|
||||
unknown-argument = "error"
|
||||
# Has a lot of warnings due to current ty walrus operator issues.
|
||||
# index-out-of-bounds = "error"
|
||||
# unresolved-attribute = "error"
|
||||
|
||||
[tool.ty.environment]
|
||||
extra-paths = [
|
||||
"src/bonsai/external_dependencies",
|
||||
"src/bcf",
|
||||
"src/bsdd",
|
||||
"src/bonsai",
|
||||
"src/ifc4d",
|
||||
"src/ifc5d",
|
||||
"src/ifccityjson",
|
||||
"src/ifcclash",
|
||||
"src/ifccsv",
|
||||
"src/ifcdiff",
|
||||
"src/ifcfm",
|
||||
"src/ifcopenshell-python",
|
||||
"src/ifcpatch",
|
||||
"src/ifctester",
|
||||
]
|
||||
|
||||
[tool.ty.src]
|
||||
exclude = [
|
||||
# External dependencies cloned for type checking only.
|
||||
"src/bonsai/external_dependencies",
|
||||
# Submodules.
|
||||
"src/ifcopenshell-python/ifcopenshell/express",
|
||||
"src/ifcopenshell-python/ifcopenshell/mvd",
|
||||
"src/ifcopenshell-python/ifcopenshell/simple_spf",
|
||||
"src/svgfill/3rdparty",
|
||||
# Has special dependencies.
|
||||
"src/ifcopenshell-python/ifcopenshell/geom/app.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/geom/code_editor_pane.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/doc.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py",
|
||||
"src/ifcopenshell-python/ifcopenshell/util/ifc4x3dev_scrape_data_for_docs.py",
|
||||
# Too esoteric.
|
||||
"src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py",
|
||||
"src/ifc2ca/templates",
|
||||
# Too dev.
|
||||
"src/bcf/setup.py",
|
||||
"src/bsdd/yml_to_classes.py",
|
||||
# Deprecated.
|
||||
"src/ifc2ca/_deprecated",
|
||||
]
|
||||
|
||||
[tool.poe.tasks]
|
||||
|
||||
ruff-main = "ruff check --extend-exclude nix/build-all.py"
|
||||
@@ -87,6 +220,47 @@ 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 = ["ty-venv-bonsai", "ty-venv-ios"]
|
||||
|
||||
ty-venv-bonsai.sequence = [
|
||||
{cmd = "uv venv src/bonsai/.venv --python=3.11 --allow-existing"},
|
||||
{cmd = "uv pip install -r src/bonsai/type-check-requirements.txt --python=src/bonsai/.venv"},
|
||||
]
|
||||
|
||||
ty-venv-ios.sequence = [
|
||||
{cmd = "uv venv src/ifcopenshell-python/.venv --python=3.10 --allow-existing"},
|
||||
{cmd = "uv pip install -r src/ifcopenshell-python/type-check-requirements.txt --python=src/ifcopenshell-python/.venv"},
|
||||
]
|
||||
|
||||
format.sequence = ["black", "ruff-main", "ruff-old"]
|
||||
|
||||
cmake-format = "gersemi . --in-place"
|
||||
|
||||
[tool.poe.tasks.ty-ios]
|
||||
# --ignore unresolved-reference: walrus operator false positives in ty.
|
||||
cmd = """
|
||||
ty check
|
||||
src/bcf
|
||||
src/bsdd
|
||||
src/ifc2ca
|
||||
src/ifc4d
|
||||
src/ifc5d
|
||||
src/ifccityjson
|
||||
src/ifcclash
|
||||
src/ifccsv
|
||||
src/ifcdiff
|
||||
src/ifcfm
|
||||
src/ifcopenshell-python
|
||||
src/ifcpatch
|
||||
src/ifctester
|
||||
--python=src/ifcopenshell-python/.venv
|
||||
--ignore unresolved-reference
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.bonsai-deps]
|
||||
help = "Clone or update Bonsai external dependencies."
|
||||
cmd = "python src/bonsai/scripts/bonsai_deps.py"
|
||||
|
||||
@@ -34,8 +34,8 @@ client_id, client_secret = "", ""
|
||||
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||
self.server.auth_code = query.get("code", [""])[0] # type: ignore
|
||||
self.server.auth_state = query.get("state", [""])[0] # type: ignore
|
||||
self.server.auth_code = query.get("code", [""])[0]
|
||||
self.server.auth_state = query.get("state", [""])[0]
|
||||
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,
|
||||
) -> list[Any]:
|
||||
) -> None:
|
||||
# return self.get(
|
||||
# f"/projects/{project_id}/topics",
|
||||
# {
|
||||
|
||||
@@ -173,16 +173,17 @@ 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,
|
||||
),
|
||||
@@ -193,6 +194,7 @@ 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",
|
||||
)
|
||||
@@ -200,16 +202,17 @@ 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,
|
||||
),
|
||||
@@ -220,6 +223,7 @@ 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",
|
||||
)
|
||||
|
||||
+1
-2
@@ -232,8 +232,7 @@ endif
|
||||
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
|
||||
|
||||
# Required for hipped roof generation
|
||||
# TODO: Use official repo once https://github.com/prochitecture/bpypolyskel/pull/22 is merged.
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/bpypolyskel.git@pyproject_toml" --no-deps -w wheels/
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/prochitecture/bpypolyskel" --no-deps -w wheels/
|
||||
|
||||
# folder for executable files
|
||||
mkdir -p build/bonsai/libs/bin
|
||||
|
||||
@@ -72,9 +72,7 @@ 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.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
|
||||
)
|
||||
self.file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
|
||||
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
||||
self.file.header.file_name.originating_system = "{} {}".format(
|
||||
self.get_application_name(), tool.Blender.get_bonsai_version()
|
||||
|
||||
@@ -45,16 +45,19 @@ 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. 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
|
||||
# The object is invalid but somehow still has a callback.
|
||||
# This can occur during undo/redo when the Python wrapper is stale.
|
||||
return
|
||||
# Blender names are up to 63 UTF-8 bytes
|
||||
if len(bytes(obj.name, "utf-8")) >= 63:
|
||||
return
|
||||
@@ -189,7 +192,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
|
||||
return
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=subscribe_to,
|
||||
owner=obj,
|
||||
owner=object_subscription_owner,
|
||||
args=(
|
||||
obj,
|
||||
data_path,
|
||||
|
||||
@@ -316,11 +316,8 @@ class IfcStore:
|
||||
del IfcStore.id_map[data["id"]]
|
||||
if "guid" in data:
|
||||
del IfcStore.guid_map[data["guid"]]
|
||||
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)
|
||||
# Note: msgbus subscriptions are cleared globally during
|
||||
# rebuild_element_maps which runs after every undo/redo.
|
||||
|
||||
@staticmethod
|
||||
def commit_link_element(data: OperationData) -> None:
|
||||
@@ -367,10 +364,8 @@ class IfcStore:
|
||||
del IfcStore.id_map[data["id"]]
|
||||
if "guid" in data:
|
||||
del IfcStore.guid_map[data["guid"]]
|
||||
obj = IfcStore.get_object_by_name(data["obj"])
|
||||
# obj might be removed after unlink.
|
||||
if not obj:
|
||||
bpy.msgbus.clear_by_owner(obj)
|
||||
# Note: msgbus subscriptions are cleared globally during
|
||||
# rebuild_element_maps which runs after every undo/redo.
|
||||
|
||||
@staticmethod
|
||||
def unlink_element(
|
||||
|
||||
@@ -64,8 +64,8 @@ class MaterialCreator:
|
||||
mesh: Union[OBJECT_DATA_TYPE, None],
|
||||
shape_has_openings: bool,
|
||||
) -> None:
|
||||
if ((rep := getattr(element, "Representation", ...) is not ...) and not rep) or (
|
||||
(rep := getattr(element, "RepresentationMaps", ...) is not ...) and not rep
|
||||
if ((rep := getattr(element, "Representation", ...)) is not ... and not rep) or (
|
||||
(rep := getattr(element, "RepresentationMaps", ...)) is not ... and not rep
|
||||
):
|
||||
return
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ class AggregateDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def dotted_line_shader(self):
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
|
||||
vert_out.smooth("FLOAT", "v_ArcLength")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
|
||||
@@ -230,7 +230,7 @@ class BcfTopic(PropertyGroup):
|
||||
|
||||
|
||||
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
global RELATED_TOPICS_ENUM_ITEMS
|
||||
global RELATED_TOPICS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
props = self
|
||||
active_topic = props.active_topic
|
||||
active_related_topics = active_topic.related_topics.keys()
|
||||
|
||||
@@ -377,6 +377,8 @@ 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"}
|
||||
|
||||
|
||||
@@ -392,6 +394,8 @@ 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"}
|
||||
|
||||
|
||||
@@ -411,6 +415,8 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = getattr(bprops, blender_property, None)
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
attributes[blender_property] = entity
|
||||
attributes["physical_or_virtual"] = bprops.physical_or_virtual
|
||||
attributes["internal_or_external"] = bprops.internal_or_external
|
||||
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
|
||||
bpy.ops.bim.disable_editing_boundary()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Union
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
PointerProperty,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -50,12 +51,43 @@ 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
|
||||
@@ -63,6 +95,8 @@ class BIMObjectBoundaryProperties(PropertyGroup):
|
||||
related_building_element: Union[bpy.types.Object, None]
|
||||
parent_boundary: Union[bpy.types.Object, None]
|
||||
corresponding_boundary: Union[bpy.types.Object, None]
|
||||
physical_or_virtual: str
|
||||
internal_or_external: str # values depend on schema: IFC2X3 omits EXTERNAL_EARTH/WATER/FIRE
|
||||
|
||||
|
||||
class BIMBoundaryProperties(PropertyGroup):
|
||||
|
||||
@@ -77,6 +77,10 @@ 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")
|
||||
@@ -84,6 +88,8 @@ 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)
|
||||
@@ -110,6 +116,11 @@ class BIM_PT_Boundary(Panel):
|
||||
else:
|
||||
row.label(text="")
|
||||
|
||||
def draw_enum_data(self, boundary, ifc_attribute: str):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=ifc_attribute)
|
||||
row.label(text=getattr(boundary, ifc_attribute, "") or "")
|
||||
|
||||
def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str):
|
||||
if hasattr(boundary, ifc_attribute):
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -46,26 +46,26 @@ def get_libraries(self, context):
|
||||
|
||||
|
||||
def get_namespaces(self, context):
|
||||
global NAMESPACES_ENUM_ITEMS
|
||||
global NAMESPACES_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
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
|
||||
global ENTITY_CLASSES_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
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
|
||||
global BRICK_ROOTS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
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
|
||||
global BRICK_RELATIONS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
|
||||
for relation in BrickschemaData.data["active_relations"]:
|
||||
if relation["predicate_name"] == "label":
|
||||
|
||||
@@ -37,6 +37,7 @@ 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):
|
||||
@@ -82,6 +83,7 @@ class CadTrimExtend(bpy.types.Operator):
|
||||
class CadMitre(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_mitre"
|
||||
bl_label = "CAD Mitre"
|
||||
bl_description = "Joins two non-parallel paths at their intersection"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -106,23 +106,37 @@ class CadTool(WorkSpaceTool):
|
||||
)
|
||||
|
||||
row = layout.row(align=True)
|
||||
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", "Joins two non-parallel paths at their intersection", ui_context
|
||||
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__.split("\n", 1)[1].strip(), 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
|
||||
)
|
||||
|
||||
elif (
|
||||
isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
|
||||
@@ -132,15 +146,21 @@ 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", "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", "Joins two non-parallel paths at their intersection", ui_context
|
||||
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__.split("\n", 1)[1].strip(), 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
|
||||
)
|
||||
|
||||
else:
|
||||
if (
|
||||
@@ -168,19 +188,37 @@ 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", "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", "Joins two non-parallel paths at their intersection", ui_context
|
||||
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), 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__, ui_context)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class CadHotkey(bpy.types.Operator):
|
||||
|
||||
@@ -1787,7 +1787,7 @@ class CutDecorator:
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
rot_checksum_bytes: bytes = eval(DecoratorData.camera_rotation_checksum)
|
||||
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9)
|
||||
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9).reshape(3, 3)
|
||||
rot_real = tool.Blender.np_array_legacy(obj.matrix_world.to_3x3())
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
|
||||
@@ -1285,7 +1285,7 @@ class SnapManager:
|
||||
continue
|
||||
|
||||
coords = np.empty(vertex_count * 3, dtype=np.float32)
|
||||
mesh.vertices.foreach_get("co", coords) # type: ignore[arg-type]
|
||||
mesh.vertices.foreach_get("co", coords)
|
||||
coords = coords.reshape(-1, 3)
|
||||
|
||||
matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
|
||||
|
||||
@@ -456,7 +456,8 @@ def format_distance(
|
||||
tx_dist = fmt % d_cm
|
||||
|
||||
else:
|
||||
tx_dist = fmt % value
|
||||
assert f"Unexpected unit_system - '{unit_system}'."
|
||||
# tx_dist = fmt % value
|
||||
|
||||
return tx_dist
|
||||
|
||||
|
||||
@@ -1426,6 +1426,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
"/Pset_.*Common/.Status",
|
||||
"EPset_Status.Status",
|
||||
"EPset_Status.UserDefinedStatus",
|
||||
"Material.Name",
|
||||
]
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
@@ -3305,9 +3306,8 @@ class AddTextLiteral(bpy.types.Operator):
|
||||
attr.data_type = "string"
|
||||
attr.string_value = literal_attr_values[attr_name]
|
||||
|
||||
box_alignment_mask = [False] * 9
|
||||
box_alignment_mask[6] = True # bottom_left box_alignment
|
||||
literal_props.box_alignment = box_alignment_mask
|
||||
literal_props.align_vertical = "bottom"
|
||||
literal_props.align_horizontal = "left"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -3365,57 +3365,55 @@ class OrderTextLiteralDown(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# Ifc Operator is unnecessary, because suboperator is handling IFC changes.
|
||||
class AssignSelectedObjectAsProduct(bpy.types.Operator):
|
||||
class AssignSelectedObjectAsProduct(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_selected_as_product"
|
||||
bl_label = "Assign Selected Object As Product"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if len(context.selected_objects) != 2:
|
||||
cls.poll_message_set("2 objects need to be selected")
|
||||
if len(context.selected_objects) < 2:
|
||||
cls.poll_message_set("At least 2 objects need to be selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
assert bpy.context.view_layer
|
||||
def _execute(self, context):
|
||||
objs = context.selected_objects[:]
|
||||
obj1, obj2 = objs
|
||||
element1 = tool.Ifc.get_entity(obj1)
|
||||
element2 = tool.Ifc.get_entity(obj2)
|
||||
assert element1 and element2
|
||||
ifc_objs = [(o, tool.Ifc.get_entity(o)) for o in objs if tool.Ifc.get_entity(o)]
|
||||
|
||||
# Check if at least one object is an IfcAnnotation
|
||||
is_annotation1 = element1.is_a("IfcAnnotation")
|
||||
is_annotation2 = element2.is_a("IfcAnnotation")
|
||||
annotations = [(o, e) for o, e in ifc_objs if e.is_a("IfcAnnotation")]
|
||||
non_annotations = [(o, e) for o, e in ifc_objs if not e.is_a("IfcAnnotation")]
|
||||
|
||||
if not (is_annotation1 or is_annotation2):
|
||||
self.report({"ERROR"}, "At least one of the selected objects must be IfcAnnotation.")
|
||||
if not annotations:
|
||||
self.report({"ERROR"}, "At least one selected object must be an IfcAnnotation.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# If both are annotations, use the currently active object as relating product
|
||||
if is_annotation1 and is_annotation2:
|
||||
if len(non_annotations) == 1:
|
||||
# One product, one or more annotations — assign all annotations to the product.
|
||||
product = non_annotations[0][1]
|
||||
elif len(non_annotations) == 0 and len(annotations) == 2:
|
||||
# Both objects are annotations — use the non-active one as the relating product.
|
||||
active_obj = context.active_object
|
||||
if active_obj == obj1:
|
||||
other_selected_object = obj1
|
||||
bpy.context.view_layer.objects.active = obj2
|
||||
if annotations[0][0] == active_obj:
|
||||
annotation_obj, annotation = annotations[0]
|
||||
product = annotations[1][1]
|
||||
else:
|
||||
other_selected_object = obj2
|
||||
bpy.context.view_layer.objects.active = obj1
|
||||
# If only one is an annotation, make it the active object
|
||||
elif is_annotation1:
|
||||
other_selected_object = obj2
|
||||
bpy.context.view_layer.objects.active = obj1
|
||||
annotation_obj, annotation = annotations[1]
|
||||
product = annotations[0][1]
|
||||
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product)
|
||||
tool.Blender.update_viewport()
|
||||
return
|
||||
else:
|
||||
other_selected_object = obj1
|
||||
bpy.context.view_layer.objects.active = obj2
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Select exactly one product object and one or more IfcAnnotation objects.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
assert (active_obj := context.active_object)
|
||||
props = tool.Drawing.get_object_assigned_product_props(active_obj)
|
||||
props.relating_product = other_selected_object
|
||||
bpy.ops.bim.edit_assigned_product()
|
||||
return {"FINISHED"}
|
||||
for annotation_obj, _ in annotations:
|
||||
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product)
|
||||
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
|
||||
class EditAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -3887,8 +3885,7 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
params = {"check_existing": False}
|
||||
image = load_image(abs_path.name, str(abs_path.parent), **params)
|
||||
image = load_image(abs_path.name, str(abs_path.parent), check_existing=False)
|
||||
|
||||
mesh = bpy.data.meshes.new(image_filepath.stem)
|
||||
obj = bpy.data.objects.new(image_filepath.stem, mesh)
|
||||
@@ -4178,10 +4175,7 @@ class SelectSimilarTextLiteralValue(bpy.types.Operator):
|
||||
should_select = True
|
||||
break
|
||||
elif self.attribute_type == "box_alignment":
|
||||
box_alignment_attr = next(
|
||||
(attr for attr in literal.attributes if attr.name == "BoxAlignment"), None
|
||||
)
|
||||
if box_alignment_attr and box_alignment_attr.string_value == self.literal_value:
|
||||
if literal.get_box_alignment() == self.literal_value:
|
||||
should_select = True
|
||||
break
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
BoolVectorProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
@@ -673,20 +672,6 @@ class BIMCameraProperties(PropertyGroup):
|
||||
return ortho_scale, aspect_ratio
|
||||
|
||||
|
||||
DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
|
||||
BOX_ALIGNMENT_POSITIONS = [
|
||||
"top-left",
|
||||
"top-middle",
|
||||
"top-right",
|
||||
"middle-left",
|
||||
"center",
|
||||
"middle-right",
|
||||
"bottom-left",
|
||||
"bottom-middle",
|
||||
"bottom-right",
|
||||
]
|
||||
|
||||
|
||||
class ElementValueRow(PropertyGroup):
|
||||
"""Represents a single element value row with category, key, and formatted value"""
|
||||
|
||||
@@ -789,40 +774,38 @@ def get_category_items_with_counts(self, context):
|
||||
|
||||
|
||||
class LiteralProps(PropertyGroup):
|
||||
def set_box_alignment(self, new_value):
|
||||
markers = new_value.count(True)
|
||||
if not markers:
|
||||
return
|
||||
|
||||
if markers > 1:
|
||||
prev_value = self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
|
||||
# looking for the first value changed to positive
|
||||
first_changed_value = next((i for i in range(9) if new_value[i] and new_value[i] != prev_value[i]), None)
|
||||
|
||||
# if nothing have changed we just keep the previous value
|
||||
if first_changed_value is None:
|
||||
return
|
||||
new_value = [False] * 9
|
||||
new_value[first_changed_value] = True
|
||||
|
||||
self["box_alignment"] = new_value
|
||||
position_string = BOX_ALIGNMENT_POSITIONS[next(i for i in range(9) if new_value[i])]
|
||||
self.attributes["BoxAlignment"].set_value(position_string)
|
||||
|
||||
def get_box_alignment(self):
|
||||
return self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
|
||||
|
||||
attributes: CollectionProperty(name="Attributes", type=Attribute)
|
||||
box_alignment: BoolVectorProperty(
|
||||
name="Box alignment", size=9, set=set_box_alignment, get=get_box_alignment, default=DEFAULT_BOX_ALIGNMENT
|
||||
)
|
||||
ifc_definition_id: IntProperty(name="IFC definition ID", default=0)
|
||||
align_horizontal: EnumProperty(
|
||||
items=[
|
||||
("left", "Left", "", "ALIGN_LEFT", 0),
|
||||
("middle", "Middle", "", "ALIGN_CENTER", 1),
|
||||
("right", "Right", "", "ALIGN_RIGHT", 2),
|
||||
],
|
||||
default="left",
|
||||
name="Horizontal Alignment",
|
||||
)
|
||||
align_vertical: EnumProperty(
|
||||
items=[
|
||||
("top", "Top", "", "ALIGN_TOP", 0),
|
||||
("middle", "Middle", "", "ALIGN_MIDDLE", 1),
|
||||
("bottom", "Bottom", "", "ALIGN_BOTTOM", 2),
|
||||
],
|
||||
default="middle",
|
||||
name="Vertical Alignment",
|
||||
)
|
||||
|
||||
def get_box_alignment(self) -> str:
|
||||
alignment = self.align_vertical + "-" + self.align_horizontal
|
||||
if alignment == "middle-middle":
|
||||
alignment = "center"
|
||||
return alignment
|
||||
|
||||
def get_literal_edited_data(self) -> dict[str, str]:
|
||||
text_data = {
|
||||
"CurrentValue": self.attributes["Literal"].string_value,
|
||||
"Literal": self.attributes["Literal"].string_value,
|
||||
"BoxAlignment": self.attributes["BoxAlignment"].string_value,
|
||||
"BoxAlignment": self.get_box_alignment(),
|
||||
}
|
||||
return text_data
|
||||
|
||||
@@ -860,12 +843,19 @@ class LiteralProps(PropertyGroup):
|
||||
if TYPE_CHECKING:
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
value: str
|
||||
box_alignment: tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]
|
||||
ifc_definition_id: int
|
||||
align_horizontal: str
|
||||
align_vertical: str
|
||||
element_value_rows: bpy.types.bpy_prop_collection_idprop[ElementValueRow]
|
||||
category_for_adding: str
|
||||
|
||||
|
||||
def update_text_alignment(self, context):
|
||||
for literal_props in self.literals:
|
||||
literal_props.align_horizontal = self.align_horizontal
|
||||
literal_props.align_vertical = self.align_vertical
|
||||
|
||||
|
||||
class BIMTextProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
literals: CollectionProperty(name="Literals", type=LiteralProps)
|
||||
@@ -899,6 +889,7 @@ class BIMTextProperties(PropertyGroup):
|
||||
],
|
||||
default="left",
|
||||
name="Horizontal Alignment",
|
||||
update=update_text_alignment,
|
||||
)
|
||||
align_vertical: EnumProperty(
|
||||
items=[
|
||||
@@ -908,6 +899,7 @@ class BIMTextProperties(PropertyGroup):
|
||||
],
|
||||
default="middle",
|
||||
name="Vertical Alignment",
|
||||
update=update_text_alignment,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -366,9 +366,6 @@ class BaseLinesShader(BaseShader):
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, gap_size=16):
|
||||
super().__init__(gap_size=gap_size)
|
||||
|
||||
def glenable(self):
|
||||
super().glenable()
|
||||
|
||||
|
||||
@@ -781,33 +781,10 @@ class BIM_PT_text(Panel):
|
||||
if other_attributes:
|
||||
bonsai.bim.helper.draw_attributes(other_attributes, box)
|
||||
|
||||
row = box.row(align=True)
|
||||
cols = [row.column(align=True) for j in range(3)]
|
||||
for j in range(9):
|
||||
cols[j % 3].prop(
|
||||
literal_props,
|
||||
"box_alignment",
|
||||
text="",
|
||||
index=j,
|
||||
icon="RADIOBUT_ON" if literal_props.box_alignment[j] else "RADIOBUT_OFF",
|
||||
)
|
||||
|
||||
col = row.column(align=True)
|
||||
alignment_label_row = col.row(align=True)
|
||||
alignment_label_row.label(text=" Text box alignment:")
|
||||
|
||||
box_alignment_value = (
|
||||
literal_props.attributes[
|
||||
next(
|
||||
(idx for idx, attr in enumerate(literal_props.attributes) if attr.name == "BoxAlignment"),
|
||||
-1,
|
||||
)
|
||||
].string_value
|
||||
if any(attr.name == "BoxAlignment" for attr in literal_props.attributes)
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
col.label(text=f" {box_alignment_value}")
|
||||
row = box.row()
|
||||
row.label(text="Alignment")
|
||||
row.prop(literal_props, "align_horizontal", text="", expand=True)
|
||||
row.prop(literal_props, "align_vertical", text="", expand=True)
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
@@ -839,7 +816,6 @@ class BIM_PT_text(Panel):
|
||||
|
||||
for i, literal_data in enumerate(text_data["Literals"]):
|
||||
box = self.layout.box()
|
||||
box.label(text=f"Literal[{i}]:")
|
||||
|
||||
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
||||
for attribute in literal_data:
|
||||
|
||||
@@ -1066,7 +1066,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
cls.poll_message_set("Only available from Outliner.")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context): # ty:ignore[override-of-final-method]
|
||||
if len(getattr(context, "selected_ids", [])) == 0:
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -2289,7 +2289,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif obj in pprops.clipping_planes_objs:
|
||||
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
||||
elif element:
|
||||
if not obj.data:
|
||||
if not obj.data or obj.type not in ("MESH", "CURVE"):
|
||||
self.report({"INFO"}, "No geometry to edit")
|
||||
elif tool.Geometry.is_locked(element):
|
||||
self.report({"ERROR"}, lock_error_message(obj.name))
|
||||
|
||||
@@ -139,7 +139,9 @@ def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.typ
|
||||
tool.Georeference.set_coordinates(
|
||||
"blender",
|
||||
ifcopenshell.util.geolocation.enh2xyz(
|
||||
*local_coordinates,
|
||||
local_coordinates[0],
|
||||
local_coordinates[1],
|
||||
local_coordinates[2],
|
||||
float(props.blender_offset_x),
|
||||
float(props.blender_offset_y),
|
||||
float(props.blender_offset_z),
|
||||
@@ -162,7 +164,9 @@ def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types
|
||||
tool.Georeference.set_coordinates(
|
||||
"blender",
|
||||
ifcopenshell.util.geolocation.enh2xyz(
|
||||
*local_coordinates,
|
||||
local_coordinates[0],
|
||||
local_coordinates[1],
|
||||
local_coordinates[2],
|
||||
float(props.blender_offset_x),
|
||||
float(props.blender_offset_y),
|
||||
float(props.blender_offset_z),
|
||||
@@ -267,6 +271,8 @@ class BIMGeoreferenceProperties(PropertyGroup):
|
||||
x_axis_ordinate: str
|
||||
x_axis_is_null: bool
|
||||
|
||||
model_is_georeferenced: bool
|
||||
model_crs: str
|
||||
model_origin: str
|
||||
model_origin_si: str
|
||||
model_project_north: str
|
||||
|
||||
@@ -27,7 +27,7 @@ from bonsai.bim.prop import StrProperty
|
||||
|
||||
class BIMCityJsonProperties(PropertyGroup):
|
||||
def get_lods(self, context):
|
||||
global LODS_ENUM_ITEMS
|
||||
global LODS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
|
||||
return LODS_ENUM_ITEMS
|
||||
|
||||
|
||||
@@ -320,7 +320,7 @@ class RadianceExporterProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global SUBCATEGORIES_ENUM_ITEMS
|
||||
global SUBCATEGORIES_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||
if self.category in spectraldb:
|
||||
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
|
||||
else:
|
||||
|
||||
@@ -102,7 +102,6 @@ class MaterialsData:
|
||||
if (style_name := s.Name) is not None
|
||||
]
|
||||
results = natsorted(results, key=lambda i: i[1])
|
||||
results.insert(0, ("-", "No Surface Style", ""))
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -210,14 +210,15 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_material_to_selected"
|
||||
bl_label = "Assign Material To Selected"
|
||||
bl_description = (
|
||||
"Assign currently selected material in Materials UI to the selected objects.\n\n"
|
||||
"ALT+CLICK to assign material as a usage."
|
||||
"Assign currently selected material in Materials UI to the selected objects.\n"
|
||||
"Occurrences automatically get usages for layer/profile sets.\n\n"
|
||||
"ALT+CLICK to assign without a usage."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty(name="Material IFC ID")
|
||||
assign_as_usage: bpy.props.BoolProperty(
|
||||
name="Assign Material As A Usage",
|
||||
default=False,
|
||||
should_auto_assign_usage: bpy.props.BoolProperty(
|
||||
name="Auto Assign Usage",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
@@ -230,25 +231,19 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
material_class = tool.Ifc.get().by_id(self.material).is_a()
|
||||
if material_class not in ("IfcMaterialProfileSet", "IfcMaterialLayerSet"):
|
||||
self.report({"ERROR"}, f"{material_class} cannot be assigned as a usage.")
|
||||
return {"CANCELLED"}
|
||||
self.assign_as_usage = True
|
||||
self.should_auto_assign_usage = False
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
material = tool.Ifc.get().by_id(self.material)
|
||||
objects = tool.Blender.get_selected_objects()
|
||||
material_type = material.is_a()
|
||||
if self.assign_as_usage:
|
||||
material_type += "Usage"
|
||||
core.assign_material(
|
||||
tool.Ifc,
|
||||
tool.Material,
|
||||
material_type=material_type,
|
||||
material_type=material.is_a(),
|
||||
objects=objects,
|
||||
material=material,
|
||||
should_auto_assign_usage=self.should_auto_assign_usage,
|
||||
)
|
||||
|
||||
|
||||
@@ -614,29 +609,25 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
attributes=attributes,
|
||||
)
|
||||
|
||||
slab_planer = slab.DumbSlabPlaner()
|
||||
wall_objs = []
|
||||
layer_sets_to_regenerate = set()
|
||||
|
||||
for obj in objects:
|
||||
obj_element = tool.Ifc.get_entity(obj)
|
||||
obj_material_usage = ifcopenshell.util.element.get_material(obj_element)
|
||||
|
||||
if obj_material_usage and obj_material_usage.is_a("IfcMaterialLayerSetUsage"):
|
||||
obj_material_usage.OffsetFromReferenceLine = material.OffsetFromReferenceLine
|
||||
obj_material_usage.DirectionSense = material.DirectionSense
|
||||
obj_material_usage.ReferenceExtent = material.ReferenceExtent
|
||||
|
||||
layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet)
|
||||
|
||||
# Save custom offset to BBIM_MaterialLayer pset
|
||||
tool.Model.save_custom_offset_to_pset(obj_element, obj)
|
||||
|
||||
# Targeted regeneration: only update this element's geometry, not
|
||||
# all elements sharing the layer set (which would corrupt unrelated instances).
|
||||
if obj_material_usage.LayerSetDirection == "AXIS3":
|
||||
slab_planer.regenerate_from_occurence(obj_element, obj_material_usage)
|
||||
elif obj_material_usage.LayerSetDirection == "AXIS2":
|
||||
wall_objs.append(obj)
|
||||
|
||||
if wall_objs:
|
||||
tool.Model.recalculate_walls(wall_objs)
|
||||
for layer_set in layer_sets_to_regenerate:
|
||||
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
|
||||
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
|
||||
|
||||
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
|
||||
if "CardinalPoint" in attributes:
|
||||
@@ -726,7 +717,11 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
self.props.material_set_item_material = str(material_set_item.Material.id())
|
||||
|
||||
self.props.material_set_item_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes(material_set_item, self.props.material_set_item_attributes)
|
||||
bonsai.bim.helper.import_attributes(
|
||||
material_set_item,
|
||||
self.props.material_set_item_attributes,
|
||||
callback=self.import_attributes_callback,
|
||||
)
|
||||
|
||||
if material_set_item.is_a("IfcMaterialProfile"):
|
||||
if material_set_item.Profile and material_set_item.Profile.ProfileName:
|
||||
@@ -734,6 +729,29 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def import_attributes_callback(
|
||||
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
|
||||
) -> None | Literal[True]:
|
||||
if data["type"] != "IfcMaterialLayer" or name != "IsVentilated" or not prop:
|
||||
return None
|
||||
|
||||
# Keep null semantics unchanged on export, but avoid an empty UI selection.
|
||||
prop.data_type = "enum"
|
||||
prop.special_type = "LOGICAL"
|
||||
prop.enum_items = json.dumps(("TRUE", "FALSE", "UNKNOWN"))
|
||||
|
||||
value = data[name]
|
||||
if value == "UNKNOWN":
|
||||
prop.enum_value = "UNKNOWN"
|
||||
elif value is None:
|
||||
# Keep visible default as FALSE, but preserve null semantics on save.
|
||||
prop.enum_value = "FALSE"
|
||||
prop.is_null = True
|
||||
else:
|
||||
prop.enum_value = "TRUE" if value else "FALSE"
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DisableEditingMaterialSetItem(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_material_set_item"
|
||||
|
||||
@@ -118,12 +118,17 @@ class BIM_PT_materials(Panel):
|
||||
row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
elif self.props.editing_material_type == "STYLE":
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "contexts", text="")
|
||||
prop_with_search(row, self.props, "styles", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
if MaterialsData.data["styles"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "contexts", text="")
|
||||
prop_with_search(row, self.props, "styles", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Styles Found")
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
|
||||
|
||||
class BIM_PT_object_material(Panel):
|
||||
|
||||
@@ -82,7 +82,7 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
|
||||
|
||||
|
||||
class BIM_OT_add_object(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_grid"
|
||||
bl_idname = "bim.add_grid"
|
||||
bl_label = "Grid"
|
||||
bl_description = "Add IfcGrid."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@@ -227,7 +227,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
|
||||
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
|
||||
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
|
||||
|
||||
if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23):
|
||||
if not all([is_parallel12, is_parallel13, is_parallel21, is_parallel23]):
|
||||
fitting_type = "WYE"
|
||||
|
||||
if not fitting_type:
|
||||
@@ -903,7 +903,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
||||
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||
radius: bpy.props.FloatProperty(
|
||||
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
|
||||
name="Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -151,29 +151,11 @@ class FilledOpeningGenerator:
|
||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
assert representation
|
||||
|
||||
# Check if mapped representation - PRESERVE the mapping structure
|
||||
if (
|
||||
representation.RepresentationType == "MappedRepresentation"
|
||||
and len(representation.Items) == 1
|
||||
and representation.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
# Store the existing RepresentationMap to reuse it
|
||||
existing_mapping_source = representation.Items[0].MappingSource
|
||||
reuse_mapped_representation = True
|
||||
else:
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
if not reuse_mapped_representation:
|
||||
# Check for library template before generating from filling
|
||||
template_rep = self.get_opening_template_from_type(filling)
|
||||
|
||||
if template_rep:
|
||||
representation = template_rep
|
||||
else:
|
||||
representation = self.generate_opening_from_filling(
|
||||
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
||||
)
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
else:
|
||||
representation = self.generate_opening_from_filling(
|
||||
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
||||
)
|
||||
|
||||
# Create mapped representation
|
||||
if reuse_mapped_representation:
|
||||
@@ -247,109 +229,38 @@ class FilledOpeningGenerator:
|
||||
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
||||
|
||||
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
# ALWAYS preserve the existing opening representation (Tessellation, SweptSolid, etc.)
|
||||
preserved_representation = None
|
||||
if opening_rep:
|
||||
if (
|
||||
opening_rep.RepresentationType == "MappedRepresentation"
|
||||
and len(opening_rep.Items) == 1
|
||||
and opening_rep.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
# For mapped representations, copy the underlying representation
|
||||
preserved_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
opening_rep.Items[0].MappingSource.MappedRepresentation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
)
|
||||
else:
|
||||
# For direct representations (non-mapped), copy them too
|
||||
preserved_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(), opening_rep, exclude=["IfcGeometricRepresentationContext"]
|
||||
)
|
||||
|
||||
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
|
||||
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
|
||||
|
||||
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
|
||||
|
||||
# Priority order for choosing representation:
|
||||
# 1. Existing occurrence with MappedRepresentation (preserve mapping!)
|
||||
# 2. Library template with Tessellation
|
||||
# 3. Preserved representation from old opening (maintain user's work)
|
||||
# 4. Generate from filling (last resort)
|
||||
|
||||
representation_to_use = None
|
||||
reuse_mapped_representation = False
|
||||
existing_mapping_source = None
|
||||
|
||||
if existing_opening_occurrence:
|
||||
representation = ifcopenshell.util.representation.get_representation(
|
||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
|
||||
if (
|
||||
representation
|
||||
and representation.RepresentationType == "MappedRepresentation"
|
||||
and len(representation.Items) == 1
|
||||
and representation.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
# PRESERVE the mapped structure - reuse the same RepresentationMap
|
||||
existing_mapping_source = representation.Items[0].MappingSource
|
||||
reuse_mapped_representation = True
|
||||
else:
|
||||
representation_to_use = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
if not representation_to_use and not reuse_mapped_representation:
|
||||
template_rep = self.get_opening_template_from_type(filling)
|
||||
if template_rep and template_rep.RepresentationType == "Tessellation":
|
||||
representation_to_use = template_rep
|
||||
|
||||
if not representation_to_use and not reuse_mapped_representation and preserved_representation:
|
||||
representation_to_use = preserved_representation
|
||||
|
||||
if not representation_to_use and not reuse_mapped_representation:
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
tool.Ifc.get(), representation=representation
|
||||
)
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
)
|
||||
else:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
if opening_obj:
|
||||
tool.Ifc.unlink(element=opening)
|
||||
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
|
||||
|
||||
filling_obj = tool.Ifc.get_object(filling)
|
||||
representation_to_use = self.generate_opening_from_filling(filling, filling_obj)
|
||||
|
||||
# Create the mapped representation
|
||||
if reuse_mapped_representation:
|
||||
# Reuse existing RepresentationMap - don't create a new one!
|
||||
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
new_mapped_item = tool.Ifc.get().create_entity(
|
||||
"IfcMappedItem",
|
||||
MappingSource=existing_mapping_source,
|
||||
MappingTarget=tool.Ifc.get().create_entity(
|
||||
"IfcCartesianTransformationOperator3D",
|
||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
|
||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 1.0, 0.0)),
|
||||
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
|
||||
Scale=1.0,
|
||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
|
||||
),
|
||||
)
|
||||
mapped_representation = tool.Ifc.get().create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
ContextOfItems=context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="MappedRepresentation",
|
||||
Items=[new_mapped_item],
|
||||
)
|
||||
else:
|
||||
representation = self.generate_opening_from_filling(filling, filling_obj)
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
tool.Ifc.get(), representation=representation_to_use
|
||||
tool.Ifc.get(), representation=representation
|
||||
)
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
)
|
||||
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
)
|
||||
|
||||
# update voided object representation...
|
||||
# update voided object representation or all it's parts if it's an aggregate
|
||||
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
|
||||
for voided_element in voided_elements:
|
||||
voided_obj = tool.Ifc.get_object(voided_element)
|
||||
@@ -363,36 +274,6 @@ class FilledOpeningGenerator:
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
def get_opening_template_from_type(
|
||||
self, filling: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""
|
||||
Check if the filling's type has a stored opening template from library import.
|
||||
"""
|
||||
element_type = ifcopenshell.util.element.get_type(filling)
|
||||
|
||||
if not element_type:
|
||||
return None
|
||||
|
||||
desc = element_type.Description
|
||||
|
||||
if not desc or "||BonsaiOpeningTemplate:" not in desc:
|
||||
return None
|
||||
|
||||
# Extract template ID
|
||||
marker = desc.split("||BonsaiOpeningTemplate:")[-1]
|
||||
template_id = int(marker.split("||")[0])
|
||||
|
||||
try:
|
||||
template_rep = tool.Ifc.get().by_id(template_id)
|
||||
# Make a copy so we don't reuse the same representation instance
|
||||
copied = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(), template_rep, exclude=["IfcGeometricRepresentationContext"]
|
||||
)
|
||||
return copied
|
||||
except:
|
||||
return None
|
||||
|
||||
def generate_opening_from_filling(
|
||||
self,
|
||||
filling: ifcopenshell.entity_instance,
|
||||
@@ -659,6 +540,16 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
||||
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
|
||||
|
||||
rep_obj = tool.Geometry.get_geometry_props().representation_obj
|
||||
if booleans:
|
||||
# Users typically select two top-level items and expect the
|
||||
# operand to be absorbed into the boolean, not remain as a
|
||||
# standalone item alongside it.
|
||||
representation = tool.Geometry.get_active_representation(rep_obj)
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
second_items_set = set(second_items)
|
||||
new_items = [i for i in representation.Items if i not in second_items_set]
|
||||
if new_items:
|
||||
representation.Items = new_items
|
||||
rep_element = tool.Ifc.get_entity(rep_obj)
|
||||
tool.Model.mark_manual_booleans(rep_element, booleans)
|
||||
tool.Geometry.reload_representation(rep_obj)
|
||||
|
||||
@@ -421,7 +421,7 @@ class PolylineOperator:
|
||||
tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state)
|
||||
|
||||
tool.Blender.update_viewport()
|
||||
return {"RUNNING_MODAL"}
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
|
||||
props = tool.Model.get_model_props()
|
||||
@@ -461,6 +461,7 @@ class PolylineOperator:
|
||||
self.tool_state.axis_method = None
|
||||
self.tool_state.plane_method = None
|
||||
self.tool_state.mode = "Mouse"
|
||||
tool.Raycast.clear_snap_objs()
|
||||
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
||||
for obj in self.visible_objs:
|
||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||
|
||||
@@ -694,10 +694,14 @@ def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[
|
||||
|
||||
new_settings = settings.copy()
|
||||
new_settings["context"] = box_context
|
||||
new_box = ifcopenshell.api.geometry.add_representation(ifc_file, should_run_listeners=False, **new_settings)
|
||||
new_box = ifcopenshell.api.geometry.add_representation(
|
||||
ifc_file,
|
||||
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||
**new_settings,
|
||||
)
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
ifc_file,
|
||||
should_run_listeners=False,
|
||||
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||
product=product,
|
||||
representation=new_box,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import copy
|
||||
from math import atan2, degrees, pi, radians
|
||||
from typing import Any, Literal, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
@@ -49,7 +49,7 @@ ProfileFrom2PointsReturn = Union[dict[str, Any], None]
|
||||
|
||||
|
||||
class DumbProfileGenerator:
|
||||
def __init__(self, relating_type):
|
||||
def __init__(self, relating_type: ifcopenshell.entity_instance):
|
||||
self.relating_type = relating_type
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
@@ -201,7 +201,7 @@ class DumbProfileGenerator:
|
||||
|
||||
|
||||
class DumbProfileRegenerator:
|
||||
def regenerate_from_profile_def(self, profile):
|
||||
def regenerate_from_profile_def(self, profile: ifcopenshell.entity_instance) -> None:
|
||||
self.file = tool.Ifc.get()
|
||||
objs = []
|
||||
if not profile:
|
||||
@@ -221,7 +221,7 @@ class DumbProfileRegenerator:
|
||||
for element in self.get_element_types_using_profile(profile):
|
||||
tool.Model.mark_thumbnail_for_update(element)
|
||||
|
||||
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
|
||||
def regenerate_from_profile(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||
self.file = ifc_file
|
||||
objs = []
|
||||
profile = settings["profile"].Profile
|
||||
@@ -233,7 +233,7 @@ class DumbProfileRegenerator:
|
||||
objs.append(obj)
|
||||
DumbProfileRecalculator().recalculate(objs)
|
||||
|
||||
def get_elements_using_profile(self, profile):
|
||||
def get_elements_using_profile(self, profile: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
results = []
|
||||
profile_sets = [
|
||||
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
||||
@@ -252,7 +252,9 @@ class DumbProfileRegenerator:
|
||||
results.extend(rel.RelatedObjects)
|
||||
return results
|
||||
|
||||
def get_element_types_using_profile(self, profile):
|
||||
def get_element_types_using_profile(
|
||||
self, profile: ifcopenshell.entity_instance
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
results = []
|
||||
profile_sets = [
|
||||
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
||||
@@ -269,12 +271,18 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.extend_profile"
|
||||
bl_label = "Extend Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
join_type: bpy.props.StringProperty()
|
||||
join_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[("-", "Unjoin", ""), ("L", "L", ""), ("V", "V", ""), ("T", "T", "")],
|
||||
default="-",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
join_type: Literal["-", "L", "V", "T"]
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = context.selected_objects
|
||||
joiner = DumbProfileJoiner()
|
||||
if not self.join_type:
|
||||
if self.join_type == "-":
|
||||
for obj in selected_objs:
|
||||
joiner.unjoin(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -626,11 +634,15 @@ class DumbProfileJoiner:
|
||||
if connection1 == "ATEND":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
self.body[1] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
|
||||
@@ -673,11 +685,15 @@ class DumbProfileJoiner:
|
||||
elif connection1 == "ATSTART":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
self.body[0] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
|
||||
@@ -721,7 +737,9 @@ class DumbProfileJoiner:
|
||||
if connection1 == "ATEND":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
self.body[1] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(
|
||||
@@ -729,7 +747,9 @@ class DumbProfileJoiner:
|
||||
furthest_plane if is_relating else closest_plane,
|
||||
z_inwards=False if is_relating else True,
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
self.clippings.append(
|
||||
@@ -742,7 +762,9 @@ class DumbProfileJoiner:
|
||||
elif connection1 == "ATSTART":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
self.body[0] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(
|
||||
@@ -750,7 +772,9 @@ class DumbProfileJoiner:
|
||||
furthest_plane if is_relating else closest_plane,
|
||||
z_inwards=False if is_relating else True,
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
self.clippings.append(
|
||||
|
||||
@@ -277,19 +277,8 @@ class DumbSlabPlaner:
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2 * pi, tolerance=0.001) else existing_x_angle
|
||||
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
offset_direction = direction_ratios.copy()
|
||||
# The extrusion depth needed to achieve a given perpendicular thickness depends on
|
||||
# how much the extrusion direction deviates from the slab face normal (local Z).
|
||||
# For an ObjectPlacement-rotated slab, extrusion_vec.z ≈ 1.0 → no scaling.
|
||||
# For an ExtrudedDirection-tilted slab, extrusion_vec.z < 1.0 → scale up.
|
||||
extrusion_z = abs(direction_ratios.normalized().z)
|
||||
if extrusion_z > 1e-6:
|
||||
perpendicular_depth = thickness / extrusion_z
|
||||
perpendicular_offset = layer_offset / extrusion_z / self.unit_scale
|
||||
else:
|
||||
perpendicular_depth = thickness
|
||||
perpendicular_offset = layer_offset / self.unit_scale
|
||||
|
||||
ifc_position = extrusion.Position
|
||||
perpendicular_depth = thickness * abs(1 / cos(existing_x_angle))
|
||||
perpendicular_offset = layer_offset * abs(1 / cos(existing_x_angle)) / self.unit_scale
|
||||
|
||||
# Check angle and z direction to determine whether the extrusion direction is positive or negative
|
||||
if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or (
|
||||
@@ -312,6 +301,7 @@ class DumbSlabPlaner:
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
|
||||
extrusion.Depth = perpendicular_depth
|
||||
|
||||
ifc_position = extrusion.Position
|
||||
position = offset_direction * perpendicular_offset
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material:
|
||||
|
||||
@@ -31,11 +31,11 @@ def calculate_quantities(usecase_path, ifc_file: ifcopenshell.file, settings):
|
||||
return
|
||||
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
|
||||
qto = ifcopenshell.api.pset.add_qto(
|
||||
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities"
|
||||
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities" # ty:ignore[unknown-argument]
|
||||
)
|
||||
ifcopenshell.api.pset.edit_qto(
|
||||
ifc_file,
|
||||
should_run_listeners=False,
|
||||
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||
qto=qto,
|
||||
properties={
|
||||
"StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days,
|
||||
|
||||
@@ -1268,27 +1268,6 @@ class DumbWallJoiner:
|
||||
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
|
||||
return wall2
|
||||
|
||||
def join_Z(self, wall1, slab2):
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(slab2)
|
||||
|
||||
for rel in element1.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
|
||||
ifcopenshell.api.geometry.disconnect_element(
|
||||
tool.Ifc.get(),
|
||||
relating_element=rel.RelatingElement,
|
||||
related_element=element1,
|
||||
)
|
||||
|
||||
ifcopenshell.api.geometry.connect_element(
|
||||
tool.Ifc.get(),
|
||||
relating_element=element2,
|
||||
related_element=element1,
|
||||
description="TOP",
|
||||
)
|
||||
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def set_axis(self, wall, p1, p2):
|
||||
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
@@ -1333,29 +1312,6 @@ class DumbWallJoiner:
|
||||
self.set_axis(element1, p1, p2)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(wall2)
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
axis2 = tool.Model.get_wall_axis(wall2)
|
||||
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
|
||||
if intersect:
|
||||
intersect, _ = intersect
|
||||
else:
|
||||
return
|
||||
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
|
||||
|
||||
ifcopenshell.api.geometry.connect_path(
|
||||
tool.Ifc.get(),
|
||||
related_element=element1,
|
||||
relating_element=element2,
|
||||
relating_connection="ATPATH",
|
||||
related_connection=connection,
|
||||
description="BUTT",
|
||||
)
|
||||
|
||||
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
||||
|
||||
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
|
||||
wall1 = tool.Ifc.get_entity(obj1)
|
||||
wall2 = tool.Ifc.get_entity(obj2)
|
||||
|
||||
@@ -943,7 +943,7 @@ class EditObjectUI:
|
||||
if "LAYER2" in AuthoringData.data["selected_material_usages"]:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_to_underside.__doc__, ui_context
|
||||
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_walls_to_underside.__doc__, ui_context
|
||||
)
|
||||
|
||||
if AuthoringData.data["is_flippable_element"]:
|
||||
|
||||
@@ -101,7 +101,7 @@ class NestDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def dotted_line_shader(self):
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
|
||||
vert_out.smooth("FLOAT", "v_ArcLength")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
|
||||
@@ -76,6 +76,7 @@ classes = (
|
||||
operator.UnlinkIfc,
|
||||
operator.UnloadLink,
|
||||
workspace.ExploreHotkey,
|
||||
operator.GenerateUVMap,
|
||||
prop.LibraryBreadcrumb,
|
||||
prop.LibraryElement,
|
||||
prop.FilterCategory,
|
||||
|
||||
@@ -178,9 +178,18 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
bl_description = (
|
||||
"Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file."
|
||||
)
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
append_all: bpy.props.BoolProperty(default=False)
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||
filter_glob: bpy.props.StringProperty(
|
||||
default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
append_all: bpy.props.BoolProperty(default=False) # pyright: ignore[reportRedeclaration]
|
||||
use_relative_path: bpy.props.BoolProperty(
|
||||
name="Use Relative Path", default=False
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filter_glob: str
|
||||
append_all: bool
|
||||
use_relative_path: bool
|
||||
|
||||
reload_previous_file = False
|
||||
|
||||
@@ -558,7 +567,11 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.append_library_element_by_query"
|
||||
bl_label = "Append Library Element By Query"
|
||||
query: bpy.props.StringProperty(name="Query")
|
||||
|
||||
query: bpy.props.StringProperty(name="Query") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
query: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -587,9 +600,16 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"Append element to the current project.\n\n"
|
||||
"ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)"
|
||||
)
|
||||
definition: bpy.props.IntProperty()
|
||||
prop_index: bpy.props.IntProperty()
|
||||
assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"})
|
||||
definition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
prop_index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
assume_unique_by_name: bpy.props.BoolProperty(
|
||||
name="Assume Unique By Name", default=True, options={"SKIP_SAVE"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
definition: int
|
||||
prop_index: int
|
||||
assume_unique_by_name: bool
|
||||
|
||||
file: ifcopenshell.file
|
||||
|
||||
@@ -618,8 +638,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not element:
|
||||
return {"FINISHED"}
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
# Store opening template from library if it exists
|
||||
self.store_opening_template_from_library(element, library_file)
|
||||
self.import_type_from_ifc(element, context)
|
||||
elif element.is_a("IfcProduct"):
|
||||
# NOTE: Non-types are not exposed in UI directly
|
||||
@@ -720,53 +738,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
|
||||
ifc_importer.create_style(element)
|
||||
|
||||
def store_opening_template_from_library(
|
||||
self, element: ifcopenshell.entity_instance, library_file: ifcopenshell.file
|
||||
) -> None:
|
||||
"""
|
||||
Find an opening representation in the library and copy it to the current file
|
||||
as a template. Store the template ID on the type for later retrieval.
|
||||
"""
|
||||
try:
|
||||
library_element = library_file.by_guid(element.GlobalId)
|
||||
except:
|
||||
return
|
||||
|
||||
# Find occurrences with openings in the library
|
||||
library_occurrences = ifcopenshell.util.element.get_types(library_element)
|
||||
|
||||
for occurrence in library_occurrences:
|
||||
if not getattr(occurrence, "FillsVoids", None):
|
||||
continue
|
||||
|
||||
library_opening = occurrence.FillsVoids[0].RelatingOpeningElement
|
||||
library_opening_rep = ifcopenshell.util.representation.get_representation(
|
||||
library_opening, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
|
||||
if not library_opening_rep:
|
||||
continue
|
||||
|
||||
# Check if mapped representation
|
||||
if (
|
||||
library_opening_rep.RepresentationType == "MappedRepresentation"
|
||||
and len(library_opening_rep.Items) == 1
|
||||
and library_opening_rep.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
|
||||
mapped_rep = library_opening_rep.Items[0].MappingSource.MappedRepresentation
|
||||
|
||||
# Store ALL representation types (Tessellation, SweptSolid, etc.)
|
||||
template_rep = ifcopenshell.util.element.copy_deep(
|
||||
self.file, mapped_rep, exclude=["IfcGeometricRepresentationContext"]
|
||||
)
|
||||
|
||||
# Store reference in type's Description
|
||||
current_desc = element.Description or ""
|
||||
element.Description = f"{current_desc}||BonsaiOpeningTemplate:{template_rep.id()}"
|
||||
return
|
||||
break
|
||||
|
||||
|
||||
class EditProjectLibrary(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_project_library"
|
||||
@@ -988,24 +959,28 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
bl_label = "Load Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Load an existing IFC project"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"})
|
||||
is_advanced: bpy.props.BoolProperty(
|
||||
filepath: bpy.props.StringProperty(
|
||||
subtype="FILE_PATH", options={"SKIP_SAVE"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
filter_glob: bpy.props.StringProperty(
|
||||
default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
is_advanced: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Enable Advanced Mode",
|
||||
description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings",
|
||||
default=False,
|
||||
)
|
||||
use_relative_path: bpy.props.BoolProperty(
|
||||
use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Use Relative Path",
|
||||
description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved",
|
||||
default=False,
|
||||
)
|
||||
should_start_fresh_session: bpy.props.BoolProperty(
|
||||
should_start_fresh_session: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Should Start Fresh Session",
|
||||
description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option",
|
||||
default=True,
|
||||
)
|
||||
import_without_ifc_data: bpy.props.BoolProperty(
|
||||
import_without_ifc_data: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Import Without IFC Data",
|
||||
description=(
|
||||
"Import IFC objects as Blender objects without any IFC metadata and authoring capabilities."
|
||||
@@ -1013,9 +988,20 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
|
||||
use_detailed_tooltip: bpy.props.BoolProperty(
|
||||
default=False, options={"HIDDEN"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
filename_ext = ".ifc"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filepath: str
|
||||
filter_glob: str
|
||||
is_advanced: bool
|
||||
use_relative_path: bool
|
||||
should_start_fresh_session: bool
|
||||
import_without_ifc_data: bool
|
||||
use_detailed_tooltip: bool
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
tooltip = cls.bl_description
|
||||
@@ -1116,7 +1102,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
else:
|
||||
return self.finish_loading_project(context)
|
||||
|
||||
def finish_loading_project(self, context):
|
||||
def finish_loading_project(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
try:
|
||||
filepath = self.get_filepath()
|
||||
if not self.is_existing_ifc_file():
|
||||
@@ -1314,7 +1300,10 @@ class ToggleFilterCategories(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_filter_categories"
|
||||
bl_label = "Toggle Filter Categories"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
should_select: bpy.props.BoolProperty(name="Should Select", default=True)
|
||||
should_select: bpy.props.BoolProperty(name="Should Select", default=True) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
should_select: bool
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
@@ -1338,6 +1327,14 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
default=False,
|
||||
)
|
||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
||||
query: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Query",
|
||||
description=(
|
||||
"Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n"
|
||||
"Default query - IfcElement, but excluding IfcProxy, IfcSpatialStructureElement, IfcSpatialElement, IfcFeatureElement."
|
||||
),
|
||||
)
|
||||
|
||||
filename_ext = ".ifc"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -1347,20 +1344,25 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
filter_glob: str
|
||||
use_relative_path: bool
|
||||
use_cache: bool
|
||||
query: str
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
pprops = tool.Project.get_project_props()
|
||||
row = self.layout.row()
|
||||
row.prop(self, "use_relative_path")
|
||||
row = self.layout.row()
|
||||
row.prop(self, "use_cache")
|
||||
row = self.layout.row()
|
||||
row.prop(pprops, "false_origin_mode")
|
||||
row.label(text="False Origin Mode:")
|
||||
row = self.layout.row()
|
||||
row.prop(pprops, "false_origin_mode", text="")
|
||||
if pprops.false_origin_mode == "MANUAL":
|
||||
row = self.layout.row()
|
||||
row.prop(pprops, "false_origin")
|
||||
row = self.layout.row()
|
||||
row.prop(pprops, "project_north")
|
||||
self.layout.prop(self, "query", placeholder="IfcElement")
|
||||
|
||||
def _execute(self, context):
|
||||
start = time.time()
|
||||
@@ -1393,7 +1395,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.name = filepath
|
||||
new.filepath = filepath
|
||||
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache)
|
||||
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache, query=self.query)
|
||||
|
||||
|
||||
class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -1401,7 +1403,11 @@ class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Unlink IFC"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Remove the selected file from the link list"
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
@@ -1421,7 +1427,11 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Unload Link"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Unload the selected linked file"
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def _execute(self, context):
|
||||
link = tool.Project.get_project_props().links[self.link_index]
|
||||
@@ -1446,10 +1456,12 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration]
|
||||
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
use_cache: bool
|
||||
query: str
|
||||
|
||||
def _execute(self, context):
|
||||
self.link = tool.Project.get_project_props().links[self.link_index]
|
||||
@@ -1491,8 +1503,20 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def link_ifc(self) -> Union[set[str], None]:
|
||||
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
|
||||
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
|
||||
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
|
||||
|
||||
if not self.use_cache and blend_filepath.exists():
|
||||
def should_clear_cache() -> bool:
|
||||
if not self.use_cache:
|
||||
return True
|
||||
if not blend_filepath.exists():
|
||||
return False
|
||||
data = json.loads(json_filepath.read_text())
|
||||
# Empty 'query' - model loaded without custom query.
|
||||
# Missing 'query' - model was loaded before custom queries were introduced in Bonsai.
|
||||
query = data.get("query", "")
|
||||
return query != self.query
|
||||
|
||||
if should_clear_cache():
|
||||
os.remove(blend_filepath)
|
||||
|
||||
if not blend_filepath.exists():
|
||||
@@ -1520,7 +1544,7 @@ def run():
|
||||
pprops.project_north = "{pprops.project_north}"
|
||||
# Use absolute path to be safe from cwd changes.
|
||||
try:
|
||||
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}")
|
||||
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}", query={repr(self.query)})
|
||||
except RuntimeError as e:
|
||||
# Operator failed (returned CANCELLED with error report)
|
||||
print(f"Failed to load linked project: {{e}}")
|
||||
@@ -1606,7 +1630,11 @@ class ReloadLink(bpy.types.Operator):
|
||||
bl_label = "Reload Link"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Reload the selected file"
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.bim.unload_link(link_index=self.link_index)
|
||||
@@ -1618,7 +1646,11 @@ class ToggleLinkSelectability(bpy.types.Operator):
|
||||
bl_label = "Toggle Link Selectability"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Toggle selectability"
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
@@ -1788,7 +1820,11 @@ class SelectLinkHandle(bpy.types.Operator):
|
||||
bl_label = "Select Link Handle"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select link empty object handle"
|
||||
link_index: bpy.props.IntProperty(name="Link Index")
|
||||
|
||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
link_index: int
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Project.get_project_props()
|
||||
@@ -1846,11 +1882,28 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".ifc"
|
||||
supported_filexts = (".ifc", ".ifczip", ".ifcjson")
|
||||
filter_glob: bpy.props.StringProperty(default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"})
|
||||
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version")
|
||||
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
|
||||
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||
filter_glob: bpy.props.StringProperty(
|
||||
default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
json_version: bpy.props.EnumProperty(
|
||||
items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version"
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
json_compact: bpy.props.BoolProperty(
|
||||
name="Export Compact IFCJSON", default=False
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
should_save_as: bpy.props.BoolProperty(
|
||||
name="Should Save As", default=False, options={"HIDDEN"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
use_relative_path: bpy.props.BoolProperty(
|
||||
name="Use Relative Path", default=False
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filter_glob: str
|
||||
json_version: str
|
||||
json_compact: bool
|
||||
should_save_as: bool
|
||||
use_relative_path: bool
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -2000,6 +2053,12 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""See ``bim.link_ifc``."""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
query: str
|
||||
|
||||
file: ifcopenshell.file
|
||||
meshes: dict[str, bpy.types.Mesh]
|
||||
# Material names is derived from diffuse as in 'r-g-b-a'.
|
||||
@@ -2049,14 +2108,17 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
tool.Loader.settings.context_settings = tool.Loader.create_settings()
|
||||
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
||||
|
||||
self.elements = set(self.file.by_type("IfcElement"))
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements |= set(self.file.by_type("IfcProxy"))
|
||||
if self.file.schema == "IFC2X3":
|
||||
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
|
||||
if self.query:
|
||||
self.elements = ifcopenshell.util.selector.filter_elements(self.file, self.query)
|
||||
else:
|
||||
self.elements |= set(self.file.by_type("IfcSpatialElement"))
|
||||
self.elements -= set(self.file.by_type("IfcFeatureElement"))
|
||||
self.elements = set(self.file.by_type("IfcElement"))
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements |= set(self.file.by_type("IfcProxy"))
|
||||
if self.file.schema == "IFC2X3":
|
||||
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
|
||||
else:
|
||||
self.elements |= set(self.file.by_type("IfcSpatialElement"))
|
||||
self.elements -= set(self.file.by_type("IfcFeatureElement"))
|
||||
|
||||
if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin:
|
||||
tool.Loader.set_manual_blender_offset(self.file)
|
||||
@@ -2081,6 +2143,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
"false_origin_mode": pprops.false_origin_mode,
|
||||
"false_origin": pprops.false_origin,
|
||||
"project_north": pprops.project_north,
|
||||
"query": self.query,
|
||||
}
|
||||
with open(self.json_filepath, "w") as f:
|
||||
json.dump(data, f)
|
||||
@@ -2495,7 +2558,7 @@ class EnableCulling(bpy.types.Operator):
|
||||
self.total_mousemoves = 0
|
||||
self.cullable_objects = []
|
||||
|
||||
def modal(self, context, event):
|
||||
def modal(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
|
||||
if not LinksData.enable_culling:
|
||||
for obj in bpy.context.visible_objects:
|
||||
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
||||
@@ -2526,7 +2589,7 @@ class EnableCulling(bpy.types.Operator):
|
||||
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def is_view_changed(self, context):
|
||||
def is_view_changed(self, context: bpy.types.Context) -> bool:
|
||||
view_matrix = context.region_data.view_matrix
|
||||
projection_matrix = context.region_data.window_matrix
|
||||
vp_matrix = projection_matrix @ view_matrix
|
||||
@@ -2541,7 +2604,7 @@ class EnableCulling(bpy.types.Operator):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_object_in_view(self, obj, context, camera_position):
|
||||
def is_object_in_view(self, obj: bpy.types.Object, context: bpy.types.Context, camera_position: Vector) -> bool:
|
||||
# Get the view matrix and the projection matrix from the active viewport
|
||||
view_matrix = context.region_data.view_matrix
|
||||
projection_matrix = context.region_data.window_matrix
|
||||
@@ -2568,7 +2631,7 @@ class EnableCulling(bpy.types.Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set["rna_enums.OperatorReturnItems"]:
|
||||
LinksData.enable_culling = True
|
||||
self.cullable_objects = []
|
||||
for obj in bpy.context.visible_objects:
|
||||
@@ -2855,8 +2918,16 @@ class IFCFileHandlerOperator(bpy.types.Operator):
|
||||
bl_label = "Import .ifc file"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
|
||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
|
||||
directory: bpy.props.StringProperty(
|
||||
subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
files: bpy.props.CollectionProperty(
|
||||
type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"}
|
||||
) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
directory: str
|
||||
files: list[bpy.types.OperatorFileListElement]
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Keeping code in .invoke() as we'll probably add some
|
||||
@@ -2907,7 +2978,10 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
|
||||
bl_label = "Measure Tool"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
measure_type: bpy.props.StringProperty()
|
||||
measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
measure_type: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -3003,7 +3077,10 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
|
||||
bl_label = "Measure Face Area Tool"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
measure_type: bpy.props.StringProperty()
|
||||
measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
measure_type: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -3107,7 +3184,10 @@ class ClearMeasurement(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
return len(polyline_props.measurement_polyline) > 0
|
||||
if len(polyline_props.measurement_polyline) > 0:
|
||||
return True
|
||||
cls.poll_message_set("No measurement to clear.")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
@@ -3211,7 +3291,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
||||
super().invoke(context, event)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def cancel_tool(self, context):
|
||||
def cancel_tool(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
context.workspace.status_text_set(text=None)
|
||||
if hasattr(self, "tool_state"):
|
||||
self.tool_state.plane_method = None
|
||||
@@ -3219,7 +3299,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
||||
tool.Blender.update_viewport()
|
||||
return {"CANCELLED"}
|
||||
|
||||
def handle_custom_instructions(self, context):
|
||||
def handle_custom_instructions(self, context: bpy.types.Context) -> None:
|
||||
if len(self.selected_points) == 0:
|
||||
instruction_text = "Click First Point on Image"
|
||||
elif len(self.selected_points) == 1:
|
||||
@@ -3234,14 +3314,14 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
||||
|
||||
context.workspace.status_text_set(text=instruction_text)
|
||||
|
||||
def calculate_distance(self):
|
||||
def calculate_distance(self) -> None:
|
||||
if len(self.selected_points) == 2:
|
||||
point1 = self.selected_points[0]
|
||||
point2 = self.selected_points[1]
|
||||
distance_3d = (point2 - point1).length
|
||||
self.calculated_distance = distance_3d / self.unit_scale
|
||||
|
||||
def apply_scaling(self, context):
|
||||
def apply_scaling(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
if len(self.selected_points) != 2:
|
||||
self.report({"ERROR"}, "Two points must be selected")
|
||||
return {"CANCELLED"}
|
||||
@@ -3299,7 +3379,10 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
||||
bl_idname = "bim.load_blend_metadata_and_ifc"
|
||||
bl_label = "Load Blend Metadata and IFC"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(name="IFC File Path", default="")
|
||||
filepath: bpy.props.StringProperty(name="IFC File Path", default="") # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filepath: str
|
||||
|
||||
def execute(self, context):
|
||||
ifc_file = self.filepath
|
||||
@@ -3333,3 +3416,19 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
||||
bpy.app.handlers.load_post.append(load_handler)
|
||||
bpy.ops.wm.open_mainfile(filepath=metadata_path)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GenerateUVMap(bpy.types.Operator):
|
||||
bl_idname = "bim.generate_uv_map"
|
||||
bl_label = "Generate UV Map"
|
||||
bl_description = "Generate UV map for selected mesh."
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj or not isinstance(obj.data, bpy.types.Mesh):
|
||||
self.report({"ERROR"}, "No valid mesh selected.")
|
||||
return {"CANCELLED"}
|
||||
tool.Loader.load_generated_uv_map(obj.data)
|
||||
self.report({"INFO"}, "Generated UV map for selected mesh.")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -496,7 +496,7 @@ class BIM_PT_links(Panel):
|
||||
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
|
||||
else:
|
||||
row.operator("bim.load_link", text="", icon="LINKED").link_index = index
|
||||
row.operator("bim.unlink_ifc", text="", icon="X").link_index = index
|
||||
row.operator("bim.unlink_ifc", text="", icon="X").link_index = index
|
||||
self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index")
|
||||
|
||||
if LinksData.enable_culling:
|
||||
|
||||
@@ -71,21 +71,26 @@ class ExploreTool(bpy.types.WorkSpaceTool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_M")
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
|
||||
op.hotkey = "S_M"
|
||||
row = layout.row(align=True)
|
||||
row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=True)
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.clear_measurement", text="", icon="X")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_S")
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE")
|
||||
op.hotkey = "S_S"
|
||||
op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them"
|
||||
op.description = (
|
||||
"Scale Image Annotation.\n\n"
|
||||
"Allows to scale an IfcReferenceImage.\n\n"
|
||||
"Select image, select tool. "
|
||||
"Check lower left corner instructions to select two points and provide real distance between them"
|
||||
)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.generate_uv_map", icon="UV")
|
||||
|
||||
|
||||
class ExploreHotkey(bpy.types.Operator):
|
||||
|
||||
@@ -262,7 +262,7 @@ class BIM_PT_object_psets(Panel):
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, props, "pset_name", text="")
|
||||
if props.pset_name != "BBIM_BSDD" and not props.pset_name.startswith(tool.Bsdd.identifier_url):
|
||||
if props.pset_name != "BBIM_BSDD" and not props.pset_name.startswith(tool.Bsdd.identifier_url()):
|
||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
||||
op.obj = obj.name
|
||||
op.obj_type = "Object"
|
||||
|
||||
@@ -321,7 +321,7 @@ def get_gross_perimeter(o: bpy.types.Object) -> float:
|
||||
return gross_perimeter
|
||||
|
||||
|
||||
def get_space_net_perimeter(obj: bpy.types.Object) -> float:
|
||||
def get_space_net_perimeter(obj: bpy.types.Object) -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -619,7 +619,7 @@ class SelectFilterElements(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ApplyFilterFromText(Operator, tool.Ifc.Operator):
|
||||
class ApplyFilterFromText(Operator):
|
||||
bl_idname = "bim.apply_filter_from_text"
|
||||
bl_label = "Apply Filter Configuration"
|
||||
bl_description = "Apply the JSON filter configuration from the current text block"
|
||||
@@ -1440,7 +1440,7 @@ class ShowAllElements(Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectSimilar(Operator, tool.Ifc.Operator):
|
||||
class SelectSimilar(Operator):
|
||||
bl_idname = "bim.select_similar"
|
||||
bl_label = "Select Similar"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@@ -251,7 +251,7 @@ class BIM_PT_grids(Panel):
|
||||
bl_options = {"HEADER_LAYOUT_EXPAND"}
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids")
|
||||
self.layout.row().operator("bim.add_grid", icon="ADD", text="Add Grids")
|
||||
|
||||
def draw_header(self, context):
|
||||
props = tool.Spatial.get_grid_props()
|
||||
|
||||
@@ -28,6 +28,7 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.unit as ifcunit
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
@@ -478,7 +479,7 @@ class ShaderInfo:
|
||||
"""get the args to the point shader"""
|
||||
location = np.array(location)
|
||||
indices = []
|
||||
direction_dict = {
|
||||
direction_dict: dict[str, tuple[npt.NDArray, ...]] = {
|
||||
"fx": (np.array((1, 0, 0)), np.array((0, 1, 0)), np.array((0, 0, 1))),
|
||||
"fy": (np.array((0, 1, 0)), np.array((1, 0, 0)), np.array((0, 0, 1))),
|
||||
"fz": (np.array((0, 0, 1)), np.array((0, 1, 0)), np.array((1, 0, 0))),
|
||||
|
||||
@@ -83,7 +83,7 @@ class DecorationShader:
|
||||
PARALLEL DISTRIBUTED FORCE,
|
||||
DISTRIBUTED MOMENT,
|
||||
"""
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
|
||||
vert_out.smooth("VEC3", "forces")
|
||||
vert_out.smooth("VEC3", "co")
|
||||
|
||||
@@ -203,7 +203,7 @@ class DecorationShader:
|
||||
"""param: pattern: type of pattern
|
||||
SINGLE FORCE,
|
||||
SINGLE MOMENT"""
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments]
|
||||
vert_out.smooth("VEC3", "co")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
@@ -253,7 +253,7 @@ class DecorationShader:
|
||||
|
||||
def get_planar_shader(self) -> gpu.types.GPUShader:
|
||||
"""shader for planar loads"""
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments]
|
||||
vert_out.smooth("VEC3", "co")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
|
||||
@@ -787,7 +787,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
doc: DocPreferences
|
||||
default_parameters: DefaultParameters
|
||||
container_hide_show_isolate: bool
|
||||
mass_time_units_in_wizard: bool
|
||||
chain_filter_with_set_operations: bool
|
||||
save_metadata_blend_file: bool
|
||||
metadata_blend_file_suffix: str
|
||||
@@ -986,7 +985,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
|
||||
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "container_hide_show_isolate")
|
||||
layout.prop(self, "mass_time_units_in_wizard")
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "chain_filter_with_set_operations")
|
||||
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
|
||||
|
||||
@@ -51,21 +51,11 @@ def add_instance_flooring_covering_from_cursor(
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
|
||||
name = "Covering"
|
||||
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
|
||||
|
||||
obj = spatial.get_named_obj_from_mesh(name, mesh)
|
||||
|
||||
obj = spatial.create_object("Covering")
|
||||
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
|
||||
spatial.translate_obj_to_z_location(obj, z)
|
||||
points = spatial.get_2d_vertices_from_obj(obj)
|
||||
points = spatial.get_scaled_2d_vertices(points)
|
||||
spatial.assign_type_to_obj(obj)
|
||||
|
||||
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
|
||||
body = spatial.get_body_representation(obj)
|
||||
spatial.regen_obj_representation(obj, body)
|
||||
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
|
||||
|
||||
|
||||
def add_instance_ceiling_covering_from_cursor(
|
||||
@@ -95,21 +85,11 @@ def add_instance_ceiling_covering_from_cursor(
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
|
||||
name = "Covering"
|
||||
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
|
||||
|
||||
obj = spatial.get_named_obj_from_mesh(name, mesh)
|
||||
|
||||
obj = spatial.create_object("Covering")
|
||||
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
|
||||
spatial.translate_obj_to_z_location(obj, z + ceiling_height)
|
||||
points = spatial.get_2d_vertices_from_obj(obj)
|
||||
points = spatial.get_scaled_2d_vertices(points)
|
||||
spatial.assign_type_to_obj(obj)
|
||||
|
||||
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
|
||||
body = spatial.get_body_representation(obj)
|
||||
spatial.regen_obj_representation(obj, body)
|
||||
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
|
||||
|
||||
|
||||
def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spatial]) -> None:
|
||||
@@ -127,19 +107,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
|
||||
|
||||
name = "Aux"
|
||||
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
|
||||
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
|
||||
obj = spatial.get_named_obj_from_mesh(name, mesh)
|
||||
|
||||
points = spatial.get_2d_vertices_from_obj(obj)
|
||||
points = spatial.get_scaled_2d_vertices(points)
|
||||
|
||||
spatial.assign_swept_area_outer_curve_from_2d_vertices(active_obj, vertices=points)
|
||||
body = spatial.get_body_representation(active_obj)
|
||||
spatial.regen_obj_representation(active_obj, body)
|
||||
spatial.set_covering_representation_from_polygon(active_obj, space_polygon, polygon_is_si=True)
|
||||
|
||||
|
||||
# TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS
|
||||
@@ -151,22 +119,13 @@ def add_instance_flooring_coverings_from_walls(root: type[tool.Root], spatial: t
|
||||
union = spatial.get_union_shape_from_selected_objects()
|
||||
for i, linear_ring in enumerate(union.interiors):
|
||||
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
|
||||
|
||||
name = "Covering" + str(i)
|
||||
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
|
||||
|
||||
spatial.set_obj_origin_to_bboxcenter(obj)
|
||||
obj = spatial.create_object(name)
|
||||
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
|
||||
spatial.translate_obj_to_z_location(obj, z)
|
||||
|
||||
points = spatial.get_2d_vertices_from_obj(obj)
|
||||
points = spatial.get_scaled_2d_vertices(points)
|
||||
|
||||
spatial.assign_type_to_obj(obj)
|
||||
|
||||
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
|
||||
body = spatial.get_body_representation(obj)
|
||||
spatial.regen_obj_representation(obj, body)
|
||||
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
|
||||
|
||||
|
||||
def add_instance_ceiling_coverings_from_walls(
|
||||
@@ -179,22 +138,13 @@ def add_instance_ceiling_coverings_from_walls(
|
||||
union = spatial.get_union_shape_from_selected_objects()
|
||||
for i, linear_ring in enumerate(union.interiors):
|
||||
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
|
||||
|
||||
name = "Covering" + str(i)
|
||||
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
|
||||
|
||||
spatial.set_obj_origin_to_bboxcenter(obj)
|
||||
obj = spatial.create_object(name)
|
||||
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
|
||||
spatial.translate_obj_to_z_location(obj, z)
|
||||
|
||||
points = spatial.get_2d_vertices_from_obj(obj)
|
||||
points = spatial.get_scaled_2d_vertices(points)
|
||||
|
||||
spatial.assign_type_to_obj(obj)
|
||||
|
||||
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
|
||||
body = spatial.get_body_representation(obj)
|
||||
spatial.regen_obj_representation(obj, body)
|
||||
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
|
||||
|
||||
|
||||
class NoDefaultContainer(Exception):
|
||||
|
||||
@@ -432,10 +432,7 @@ def update_drawing_name(
|
||||
if drawing_tool.get_name(drawing) != name:
|
||||
ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name})
|
||||
|
||||
# Update the camera object name
|
||||
camera = ifc.get_object(drawing)
|
||||
if camera and camera.name != name:
|
||||
camera.name = name
|
||||
drawing_tool.set_camera_name(drawing, name)
|
||||
|
||||
group = drawing_tool.get_drawing_group(drawing)
|
||||
if drawing_tool.get_name(group) != name:
|
||||
|
||||
@@ -113,6 +113,7 @@ def assign_material(
|
||||
material_type: Union[str, None],
|
||||
objects: list[bpy.types.Object],
|
||||
material: Optional[ifcopenshell.entity_instance] = None,
|
||||
should_auto_assign_usage: bool = True,
|
||||
) -> None:
|
||||
"""Assign material to the provided objects.
|
||||
|
||||
@@ -121,12 +122,18 @@ def assign_material(
|
||||
"""
|
||||
material_type = material_type or material_tool.get_object_ui_material_type()
|
||||
material = material or material_tool.get_object_ui_active_material()
|
||||
can_be_usage = should_auto_assign_usage and material_type in ("IfcMaterialLayerSet", "IfcMaterialProfileSet")
|
||||
for obj in objects:
|
||||
element = ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
|
||||
ifc.run("material.assign_material", products=[element], type=material_type, material=material)
|
||||
if can_be_usage and not material_tool.is_type_product(element):
|
||||
element_material_type = material_type + "Usage"
|
||||
else:
|
||||
element_material_type = material_type
|
||||
|
||||
ifc.run("material.assign_material", products=[element], type=element_material_type, material=material)
|
||||
assigned_material = material_tool.get_material(element)
|
||||
assert assigned_material # Type checker.
|
||||
|
||||
@@ -136,7 +143,9 @@ def assign_material(
|
||||
material_tool.add_material_to_set(material_set=material, material=default_material)
|
||||
elif material_tool.is_a_material_set(assigned_material):
|
||||
material_tool.add_material_to_set(material_set=assigned_material, material=material)
|
||||
material_tool.ensure_material_assigned(elements=[element], material_type=material_type, material=material)
|
||||
material_tool.ensure_material_assigned(
|
||||
elements=[element], material_type=element_material_type, material=material
|
||||
)
|
||||
|
||||
|
||||
def unassign_material(ifc: type[tool.Ifc], material_tool: type[tool.Material], objects: list[bpy.types.Object]) -> None:
|
||||
|
||||
@@ -109,7 +109,7 @@ def align_walls(
|
||||
align_type: AlignType,
|
||||
):
|
||||
reference_obj = blender.get_active_object(is_selected=True)
|
||||
if not (e := ifc.get_entity(reference_obj) or not model.get_usage_type(e) == "LAYER2"):
|
||||
if not reference_obj or not (e := ifc.get_entity(reference_obj)) or not model.get_usage_type(e) == "LAYER2":
|
||||
reference_obj = None
|
||||
objs = [
|
||||
o
|
||||
@@ -159,48 +159,6 @@ def extend_wall_to_slab(
|
||||
model.reload_body_representation(wall_objs)
|
||||
|
||||
|
||||
def join_walls_TZ(
|
||||
ifc: type[tool.Ifc],
|
||||
blender: type[tool.Blender],
|
||||
geometry: type[tool.Geometry],
|
||||
joiner: DumbWallJoiner,
|
||||
model: type[tool.Model],
|
||||
) -> None:
|
||||
selected_objs = [
|
||||
o
|
||||
for o in blender.get_selected_objects()
|
||||
if (e := ifc.get_entity(o)) and model.get_usage_type(e) in ("LAYER2", "LAYER3")
|
||||
]
|
||||
if len(selected_objs) < 2:
|
||||
raise RequireAtLeastTwoLayeredElements(
|
||||
"Two or more vertically or horizontally layered elements must be selected to connect their paths together"
|
||||
)
|
||||
|
||||
for obj in selected_objs:
|
||||
geometry.clear_scale(obj)
|
||||
|
||||
elements = [ifc.get_entity(o) for o in blender.get_selected_objects()]
|
||||
layer2_elements = []
|
||||
layer3_elements = []
|
||||
for element in elements:
|
||||
usage = model.get_usage_type(element)
|
||||
if usage == "LAYER2":
|
||||
layer2_elements.append(element)
|
||||
elif usage == "LAYER3":
|
||||
layer3_elements.append(element)
|
||||
if layer3_elements:
|
||||
target = ifc.get_object(layer3_elements[0])
|
||||
for element in layer2_elements:
|
||||
joiner.join_Z(ifc.get_object(element), target)
|
||||
else:
|
||||
if not (active_obj := blender.get_active_object()):
|
||||
active_obj = selected_objs[0]
|
||||
for obj in selected_objs:
|
||||
if obj == active_obj:
|
||||
continue
|
||||
joiner.join_T(obj, active_obj)
|
||||
|
||||
|
||||
class RequireTwoWallsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -219,13 +219,8 @@ def generate_space(
|
||||
else:
|
||||
assert space_polygon
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=h, polygon_is_si=True)
|
||||
|
||||
mesh = spatial.get_named_mesh_from_bmesh(name="Space", bmesh=bm)
|
||||
|
||||
if element and element.is_a("IfcSpace"):
|
||||
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
|
||||
spatial.edit_active_space_obj_from_mesh(mesh)
|
||||
spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True)
|
||||
spatial.translate_obj_to_z_location(active_obj, z)
|
||||
else:
|
||||
if relating_type:
|
||||
@@ -233,12 +228,13 @@ def generate_space(
|
||||
else:
|
||||
name = "Space"
|
||||
|
||||
obj = spatial.get_named_obj_from_mesh(name, mesh)
|
||||
obj = spatial.create_object(name)
|
||||
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
|
||||
spatial.translate_obj_to_z_location(obj, z)
|
||||
spatial.assign_ifcspace_class_to_obj(obj)
|
||||
|
||||
element = ifc.get_entity(obj)
|
||||
spatial.set_space_representation_from_polygon(obj, element, space_polygon, h, polygon_is_si=True)
|
||||
|
||||
if relating_type:
|
||||
spatial.assign_relating_type_to_element(ifc, type, element, relating_type)
|
||||
@@ -257,16 +253,16 @@ def generate_spaces_from_walls(
|
||||
for i, linear_ring in enumerate(union.interiors):
|
||||
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h, polygon_is_si=False)
|
||||
|
||||
name = "Space" + str(i)
|
||||
|
||||
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
|
||||
|
||||
spatial.set_obj_origin_to_bboxcenter_and_zero_elevation(obj)
|
||||
obj = spatial.create_object(name)
|
||||
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
|
||||
spatial.translate_obj_to_z_location(obj, z)
|
||||
spatial.assign_ifcspace_class_to_obj(obj)
|
||||
|
||||
element = ifc.get_entity(obj)
|
||||
spatial.set_space_representation_from_polygon(obj, element, poly, h, polygon_is_si=False)
|
||||
|
||||
|
||||
def toggle_space_visibility(ifc: type[tool.Ifc], spatial: type[tool.Spatial]) -> None:
|
||||
model = ifc.get()
|
||||
|
||||
@@ -349,7 +349,10 @@ class Drawing:
|
||||
def enable_editing_text(cls, obj): pass
|
||||
def ensure_unique_drawing_name(cls, name): pass
|
||||
def ensure_unique_identification(cls, identification): pass
|
||||
def export_font_size(cls, obj): pass
|
||||
def export_symbol(cls, obj): pass
|
||||
def export_text_literal_attributes(cls, obj): pass
|
||||
def export_wrap_length(cls, obj): pass
|
||||
def generate_drawing_matrix(cls, target_view, location_hint): pass
|
||||
def generate_drawing_name(cls, target_view, location_hint): pass
|
||||
def generate_reference_attributes(cls, reference, **attributes): pass
|
||||
@@ -402,6 +405,7 @@ class Drawing:
|
||||
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
|
||||
def run_type_assign_type(cls, element=None, relating_type=None): pass
|
||||
def select_assigned_product(cls, drawing): pass
|
||||
def set_camera_name(cls, drawing, name): pass
|
||||
def set_drawing_collection_name(cls, drawing, collection): pass
|
||||
def set_name(cls, element, name): pass
|
||||
def setup_annotation_object(cls, obj, object_type): pass
|
||||
@@ -580,6 +584,7 @@ class Material:
|
||||
def import_material_definitions(cls, material_type: str): pass
|
||||
def is_a_flow_segment(cls, element): pass
|
||||
def is_a_material_set(cls, material): pass
|
||||
def is_type_product(cls, element): pass
|
||||
def is_editing_materials(cls): pass
|
||||
def is_material_used_in_sets(cls, material): pass
|
||||
def load_material_attributes(cls, material): pass
|
||||
@@ -619,7 +624,10 @@ class Model:
|
||||
def import_rectangle(cls, obj, position, profile): pass
|
||||
def load_openings(cls, openings): pass
|
||||
def purge_scene_openings(cls): pass
|
||||
def recalculate_walls(cls, objs): pass
|
||||
def regenerate_array(cls, parent, data): pass
|
||||
def regenerate_profile(cls, obj): pass
|
||||
def regenerate_slab(cls, obj): pass
|
||||
def reload_body_representation(cls, obj_or_objects): pass
|
||||
def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass
|
||||
|
||||
@@ -991,14 +999,12 @@ class Spatial:
|
||||
def get_purged_inner_holes_poly(cls, union_geom, min_area): pass
|
||||
def get_poly_valid_interior_list(cls, poly, min_area, interiors_list): pass
|
||||
def get_buffered_poly_from_linear_ring(cls, linear_ring): pass
|
||||
def get_bmesh_from_polygon(cls, poly, h, polygon_is_si=False): pass
|
||||
def get_named_obj_from_bmesh(cls, name, bmesh): pass
|
||||
def get_named_obj_from_mesh(cls, name, mesh): pass
|
||||
def get_named_mesh_from_bmesh(cls, name, bmesh): pass
|
||||
def get_transformed_mesh_from_local_to_global(cls, mesh): pass
|
||||
def edit_active_space_obj_from_mesh(cls, mesh): pass
|
||||
def set_obj_origin_to_bboxcenter(cls, obj): pass
|
||||
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj): pass
|
||||
def get_2d_vertices_from_polygon(cls, poly, obj, polygon_is_si=True): pass
|
||||
def set_extrusion_representation_from_polygon(cls, obj, element, poly, depth_ifc, polygon_is_si=True): pass
|
||||
def set_space_representation_from_polygon(cls, obj, element, poly, h, polygon_is_si=True): pass
|
||||
def set_covering_representation_from_polygon(cls, obj, poly, polygon_is_si=True): pass
|
||||
def create_object(cls, name): pass
|
||||
def set_obj_origin_to_polygon_center(cls, obj, poly, polygon_is_si=True): pass
|
||||
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj): pass
|
||||
def get_selected_objects(cls): pass
|
||||
def get_active_obj(cls): pass
|
||||
@@ -1006,14 +1012,9 @@ class Spatial:
|
||||
def get_active_obj_height(cls): pass
|
||||
def get_relating_type_id(cls): pass
|
||||
def translate_obj_to_z_location(cls, obj, z): pass
|
||||
def get_2d_vertices_from_obj(cls, obj): pass
|
||||
def get_scaled_2d_vertices(cls, points): pass
|
||||
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass
|
||||
def get_body_representation(cls, obj): pass
|
||||
def assign_ifcspace_class_to_obj(cls, obj): pass
|
||||
def assign_type_to_obj(cls, obj): pass
|
||||
def assign_relating_type_to_element(cls, ifc, type, element, relating_type): pass
|
||||
def regen_obj_representation(cls, obj, body): pass
|
||||
def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass
|
||||
def toggle_hide_spaces(cls, spaces): pass
|
||||
def set_default_container(cls, container): pass
|
||||
@@ -1118,6 +1119,8 @@ class Type:
|
||||
def get_representation_context(cls, representation): pass
|
||||
def get_type_occurrences(cls, element_type): pass
|
||||
def has_material_usage(cls, element): pass
|
||||
def record_material_usage_attributes(cls, element): pass
|
||||
def restore_material_usage_attributes(cls, element, usage_attributes): pass
|
||||
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
|
||||
def run_geometry_switch_representation(cls, obj=None, representation=None): pass
|
||||
|
||||
|
||||
@@ -49,21 +49,40 @@ class Aggregate(bonsai.core.tool.Aggregate):
|
||||
related_object = tool.Ifc.get_entity(related_obj)
|
||||
if not relating_object or not related_object:
|
||||
return False
|
||||
if relating_object == related_object:
|
||||
return False
|
||||
|
||||
is_compatible_class = False
|
||||
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a(
|
||||
"IfcElement"
|
||||
):
|
||||
return True
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
is_compatible_class = True
|
||||
elif tool.Ifc.get_schema() == "IFC2X3":
|
||||
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
|
||||
return True
|
||||
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
|
||||
return True
|
||||
is_compatible_class = True
|
||||
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
|
||||
is_compatible_class = True
|
||||
else:
|
||||
if relating_object.is_a("IfcSpatialElement") and related_object.is_a("IfcSpatialElement"):
|
||||
return True
|
||||
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
|
||||
return True
|
||||
return False
|
||||
is_compatible_class = True
|
||||
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
|
||||
is_compatible_class = True
|
||||
|
||||
if not is_compatible_class:
|
||||
return False
|
||||
|
||||
# Prevent cyclic references: walk up the full hierarchy from the
|
||||
# proposed parent and reject if we encounter the proposed child.
|
||||
ancestor = ifcopenshell.util.element.get_parent(relating_object)
|
||||
seen = {relating_object}
|
||||
while ancestor:
|
||||
if ancestor == related_object:
|
||||
return False
|
||||
if ancestor in seen:
|
||||
break
|
||||
seen.add(ancestor)
|
||||
ancestor = ifcopenshell.util.element.get_parent(ancestor)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||
|
||||
@@ -32,14 +32,28 @@ import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bsdd.bsdd import ClassContractV1, ClassPropertyContractV1, PropertyContractV5
|
||||
|
||||
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDDictionary
|
||||
|
||||
|
||||
class Bsdd(bonsai.core.tool.Bsdd):
|
||||
identifier_url = "https://identifier.buildingsmart.org"
|
||||
default_identifier_url = "https://identifier.buildingsmart.org"
|
||||
default_api_url = "https://api.bsdd.buildingsmart.org/api/"
|
||||
client = bsdd.Client()
|
||||
bsdd_classes: dict[str, dict] = {}
|
||||
bsdd_properties: dict[str, dict] = {}
|
||||
bsdd_classes: dict[str, ClassContractV1] = {}
|
||||
bsdd_properties: dict[str, ClassPropertyContractV1 | PropertyContractV5] = {}
|
||||
|
||||
@classmethod
|
||||
def identifier_url(cls) -> str:
|
||||
"""Derives the identifier base URL from the current client baseurl.
|
||||
Falls back to the standard bSDD identifier URL when using the default API."""
|
||||
if cls.client.baseurl == cls.default_api_url:
|
||||
return cls.default_identifier_url
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(cls.client.baseurl)
|
||||
return f"{parsed.scheme}://{parsed.netloc}"
|
||||
|
||||
@classmethod
|
||||
def get_bsdd_props(cls) -> BIMBSDDProperties:
|
||||
@@ -255,7 +269,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
@classmethod
|
||||
def get_bsdd_property(cls, uri: str) -> dict:
|
||||
if not (bsdd_property := cls.bsdd_properties.get(uri, {})):
|
||||
bsdd_property = cls.client.get_property(uri, include_classes=True)
|
||||
# Cache miss occurs for keyword search mode, for classes cache is prepopulated.
|
||||
bsdd_property = cls.client.get_property(uri)
|
||||
cls.bsdd_properties[uri] = bsdd_property
|
||||
return bsdd_property
|
||||
|
||||
@@ -269,7 +284,7 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
for obj in tool.Blender.get_selected_objects(include_active=True):
|
||||
if element := tool.Ifc.get_entity(obj):
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
if (uri := reference.Location) and uri.startswith(cls.identifier_url):
|
||||
if (uri := reference.Location) and uri.startswith(cls.identifier_url()):
|
||||
classes.add((reference[1] or reference[2] or "Unnamed", uri))
|
||||
|
||||
dictionary_uris = (
|
||||
@@ -383,7 +398,7 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
def get_applicable_psets(cls, element: ifcopenshell.entity_instance):
|
||||
uris = set()
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
if (uri := reference.Location) and uri.startswith(cls.identifier_url):
|
||||
if (uri := reference.Location) and uri.startswith(cls.identifier_url()):
|
||||
uris.add(uri)
|
||||
psets = set()
|
||||
for uri in uris:
|
||||
@@ -399,7 +414,7 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
def is_applicable(cls, pset_uri: str, element: ifcopenshell.entity_instance) -> bool:
|
||||
uris = set()
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
if (uri := reference.Location) and uri.startswith(cls.identifier_url):
|
||||
if (uri := reference.Location) and uri.startswith(cls.identifier_url()):
|
||||
uris.add(uri)
|
||||
class_uri, pset_name = pset_uri.rsplit("#", 1)
|
||||
return class_uri in uris
|
||||
|
||||
@@ -154,7 +154,7 @@ class Cost(bonsai.core.tool.Cost):
|
||||
device = aud.Device()
|
||||
# chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/
|
||||
filepath = tool.Blender.get_data_dir_path("chaching.mp3").__str__()
|
||||
sound = aud.Sound(filepath)
|
||||
sound = aud.Sound(filepath) # ty:ignore[too-many-positional-arguments]
|
||||
device.play(sound)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -603,6 +603,10 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
for literal_props in props.literals:
|
||||
literal_data = bonsai.bim.helper.export_attributes(literal_props.attributes)
|
||||
alignment = literal_props.align_vertical + "-" + literal_props.align_horizontal
|
||||
if alignment == "middle-middle":
|
||||
alignment = "center"
|
||||
literal_data["BoxAlignment"] = alignment
|
||||
literals.append(literal_data)
|
||||
return literals
|
||||
|
||||
@@ -853,6 +857,8 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
@classmethod
|
||||
def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None:
|
||||
if not literal_attributes:
|
||||
return
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
assert (rep := cls.get_annotation_representation(element))
|
||||
to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
|
||||
@@ -1176,22 +1182,26 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
@classmethod
|
||||
def import_text_attributes(cls, obj: bpy.types.Object) -> None:
|
||||
from bonsai.bim.module.drawing.prop import BOX_ALIGNMENT_POSITIONS
|
||||
|
||||
props = cls.get_text_props(obj)
|
||||
props.literals.clear()
|
||||
|
||||
ifc_literals = cls.get_text_literal(obj, return_list=True)
|
||||
assert isinstance(ifc_literals, list)
|
||||
|
||||
if ifc_literals:
|
||||
first_alignment = getattr(ifc_literals[0], "BoxAlignment", None) or "bottom-left"
|
||||
if first_alignment == "center":
|
||||
first_alignment = "middle-middle"
|
||||
props.align_vertical, props.align_horizontal = first_alignment.split("-")
|
||||
|
||||
for ifc_literal in ifc_literals:
|
||||
literal_props = props.literals.add()
|
||||
bonsai.bim.helper.import_attributes(ifc_literal, literal_props.attributes)
|
||||
|
||||
box_alignment_mask = [False] * 9
|
||||
position_string = literal_props.attributes["BoxAlignment"].string_value
|
||||
box_alignment_mask[BOX_ALIGNMENT_POSITIONS.index(position_string)] = True
|
||||
|
||||
literal_props.box_alignment = box_alignment_mask # pyright: ignore[reportAttributeAccessIssue]
|
||||
alignment = getattr(ifc_literal, "BoxAlignment", None) or "bottom-left"
|
||||
if alignment == "center":
|
||||
alignment = "middle-middle"
|
||||
literal_props.align_vertical, literal_props.align_horizontal = alignment.split("-")
|
||||
literal_props.ifc_definition_id = ifc_literal.id()
|
||||
|
||||
from bonsai.bim.module.drawing.data import DecoratorData
|
||||
@@ -1280,6 +1290,12 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
def get_representation(cls, element, context):
|
||||
return ifcopenshell.util.representation.get_representation(element, context)
|
||||
|
||||
@classmethod
|
||||
def set_camera_name(cls, drawing: ifcopenshell.entity_instance, name: str) -> None:
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
if camera and camera.name != name:
|
||||
camera.name = name
|
||||
|
||||
@classmethod
|
||||
def set_drawing_collection_name(
|
||||
cls, drawing: ifcopenshell.entity_instance, collection: bpy.types.Collection
|
||||
|
||||
@@ -1407,8 +1407,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
:param representation_item: item to remove.
|
||||
:param element: item's element. Is used to unmark manual booleans.
|
||||
"""
|
||||
# NOTE: we assume it's not the last representation item
|
||||
# otherwise we probably would need to remove representation too
|
||||
# NOTE: a lot of shared code with `geometry.remove_representation`
|
||||
ifc_file = tool.Ifc.get()
|
||||
shape_aspects: list[ifcopenshell.entity_instance] = []
|
||||
@@ -1467,7 +1465,10 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
cls.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
|
||||
|
||||
if representation:
|
||||
representation.Items = tuple(set(representation.Items) - {representation_item})
|
||||
new_items = tuple(set(representation.Items) - {representation_item})
|
||||
if not new_items:
|
||||
return
|
||||
representation.Items = new_items
|
||||
also_consider = list(consider_inverses)
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
|
||||
|
||||
|
||||
@@ -197,12 +197,16 @@ class Ifc(bonsai.core.tool.Ifc):
|
||||
if not cls.get():
|
||||
return
|
||||
|
||||
# Clear all per-object msgbus subscriptions at once using the dedicated
|
||||
# owner. After undo/redo, per-object Python wrappers have new
|
||||
# identities so clearing by individual obj would miss stale
|
||||
# subscriptions registered with the old wrappers.
|
||||
bpy.msgbus.clear_by_owner(bonsai.bim.handler.object_subscription_owner)
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj.library:
|
||||
continue
|
||||
|
||||
bpy.msgbus.clear_by_owner(obj)
|
||||
|
||||
element = cls.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
@@ -217,8 +221,6 @@ class Ifc(bonsai.core.tool.Ifc):
|
||||
if obj.library:
|
||||
continue
|
||||
|
||||
bpy.msgbus.clear_by_owner(obj)
|
||||
|
||||
style = cls.get_entity(obj)
|
||||
if not style:
|
||||
continue
|
||||
|
||||
@@ -284,7 +284,7 @@ class IfcGit:
|
||||
if re.match("^Ifc", obj.name):
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
bpy.data.orphans_purge(do_recursive=True)
|
||||
bpy.data.orphans_purge(do_recursive=True) # ty:ignore[unknown-argument]
|
||||
|
||||
settings = import_ifc.IfcImportSettings.factory(bpy.context, path_ifc, logging.getLogger("ImportIFC"))
|
||||
settings.should_setup_viewport_camera = False
|
||||
|
||||
@@ -1063,16 +1063,12 @@ class Loader(bonsai.core.tool.Loader):
|
||||
|
||||
@classmethod
|
||||
def slice_layerset_mesh(cls, element: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> bpy.types.Mesh:
|
||||
# Always compute unit_scale fresh — cls.unit_scale may be stale (e.g. during live
|
||||
# geometry updates that don't go through the full import pipeline).
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
if not (material := ifcopenshell.util.element.get_material(element)):
|
||||
return mesh
|
||||
elif material.is_a("IfcMaterialLayerSetUsage"):
|
||||
usage = material
|
||||
layer_set = material.ForLayerSet
|
||||
offset = usage.OffsetFromReferenceLine * unit_scale
|
||||
offset = usage.OffsetFromReferenceLine * cls.unit_scale
|
||||
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
||||
else:
|
||||
return mesh
|
||||
@@ -1081,32 +1077,14 @@ class Loader(bonsai.core.tool.Loader):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
prev_co = None
|
||||
depth_scale = 1.0
|
||||
if usage.LayerSetDirection == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
# Bisect planes are always horizontal (world Z) for AXIS3.
|
||||
# For well-formed IFC data, the mesh local Z span equals total_perp_thickness
|
||||
# (extrusion.Depth is always set to thickness / extrusion_vec.z so that
|
||||
# extrusion.Depth × extrusion_vec.z = thickness). depth_scale is kept as
|
||||
# a safety net for IFC files from other authoring tools where the extrusion
|
||||
# depth may not exactly match the sum of LayerThicknesses.
|
||||
extrusion_vec = cls.get_extrusion_vector(element).normalized()
|
||||
ifc_extrusion_depth = None
|
||||
if body_rep := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body_rep).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
ifc_extrusion_depth = item.Depth
|
||||
break
|
||||
total_perp_thickness = sum(l.LayerThickness for l in layer_set.MaterialLayers)
|
||||
if ifc_extrusion_depth and total_perp_thickness:
|
||||
depth_scale = abs(extrusion_vec.z) * (ifc_extrusion_depth / total_perp_thickness)
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
@@ -1123,7 +1101,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
if i != last_i:
|
||||
prev_co = co.copy()
|
||||
co += no * layer.LayerThickness * depth_scale * unit_scale
|
||||
co += no * layer.LayerThickness * cls.unit_scale
|
||||
bisect_geom = bmesh.ops.bisect_plane(
|
||||
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
|
||||
)
|
||||
@@ -1137,17 +1115,14 @@ class Loader(bonsai.core.tool.Loader):
|
||||
for face in bisect_geom["geom"]:
|
||||
if isinstance(face, bmesh.types.BMFace):
|
||||
center = face.calc_center_median()
|
||||
dot = (center - co).dot(no)
|
||||
if dot >= 0:
|
||||
if (center - co).dot(no) >= 0:
|
||||
face.material_index = material_index
|
||||
has_layer_styles = True
|
||||
else:
|
||||
for face in bisect_geom["geom"]:
|
||||
if isinstance(face, bmesh.types.BMFace):
|
||||
center = face.calc_center_median()
|
||||
dot_co = (center - co).dot(no)
|
||||
dot_prev = (center - prev_co).dot(no)
|
||||
if dot_co < 0 and dot_prev >= 0:
|
||||
if (center - co).dot(no) < 0 and (center - prev_co).dot(no) >= 0:
|
||||
face.material_index = material_index
|
||||
has_layer_styles = True
|
||||
|
||||
|
||||
@@ -226,6 +226,10 @@ class Material(bonsai.core.tool.Material):
|
||||
"IfcMaterialProfileSet",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def is_type_product(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||
return element.is_a("IfcTypeProduct")
|
||||
|
||||
@classmethod
|
||||
def add_material_to_set(
|
||||
cls, material_set: ifcopenshell.entity_instance, material: ifcopenshell.entity_instance
|
||||
|
||||
@@ -45,9 +45,23 @@ class Nest(bonsai.core.tool.Nest):
|
||||
related_object = tool.Ifc.get_entity(related_obj)
|
||||
if not relating_object or not related_object:
|
||||
return False
|
||||
if relating_object.is_a("IfcElement") and related_object.is_a("IfcElement"):
|
||||
return True
|
||||
return False
|
||||
if relating_object == related_object:
|
||||
return False
|
||||
is_compatible_class = relating_object.is_a("IfcElement") and related_object.is_a("IfcElement")
|
||||
if not is_compatible_class:
|
||||
return False
|
||||
# Prevent cyclic references: walk up the full hierarchy from the
|
||||
# proposed parent and reject if we encounter the proposed child.
|
||||
ancestor = ifcopenshell.util.element.get_parent(relating_object)
|
||||
seen = {relating_object}
|
||||
while ancestor:
|
||||
if ancestor == related_object:
|
||||
return False
|
||||
if ancestor in seen:
|
||||
break
|
||||
seen.add(ancestor)
|
||||
ancestor = ifcopenshell.util.element.get_parent(ancestor)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def disable_editing(cls, obj: bpy.types.Object) -> None:
|
||||
|
||||
@@ -16,6 +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
|
||||
|
||||
import math
|
||||
from typing import Union
|
||||
|
||||
import bmesh
|
||||
@@ -26,7 +29,7 @@ from mathutils import Vector
|
||||
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
class Raycast(bonsai.core.tool.Raycast):
|
||||
offset = 10
|
||||
@@ -41,6 +44,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
(0, -offset),
|
||||
(offset, -offset),
|
||||
)
|
||||
snap_objs = []
|
||||
|
||||
@classmethod
|
||||
def get_visible_objects(cls, context: bpy.types.Context):
|
||||
@@ -69,8 +73,10 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
rv3d = context.region_data
|
||||
assert rv3d
|
||||
view_location = rv3d.view_matrix.inverted().translation
|
||||
view_normal = rv3d.view_rotation @ mathutils.Vector((0.0, 0.0, -1.0))
|
||||
obj_matrix = obj.matrix_world.copy()
|
||||
bbox = [obj_matrix @ Vector(v) for v in obj.bound_box]
|
||||
bbox_edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)]
|
||||
|
||||
transposed_bbox: list[Vector] = []
|
||||
bbox_2d: list[float] = []
|
||||
@@ -94,8 +100,25 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
|
||||
for v in bbox:
|
||||
coord_2d = tool.Cad.location_3d_to_region_2d_np(context.region, context.space_data.region_3d, v)
|
||||
if coord_2d is not None:
|
||||
transposed_bbox.append(coord_2d)
|
||||
transposed_bbox.append(coord_2d)
|
||||
|
||||
if not any(transposed_bbox):
|
||||
transposed_bbox = []
|
||||
# If there are None values in transposed_bbox it means that there are vertices behind the camera
|
||||
# so we get the intersection of the edge with the region border
|
||||
# new_bbox = []
|
||||
if any(transposed_bbox) and not all(transposed_bbox):
|
||||
new_bbox = transposed_bbox.copy()
|
||||
new_bbox = [x for x in new_bbox if x is not None]
|
||||
for edge in bbox_edges:
|
||||
if (transposed_bbox[edge[0]] is None) ^ (transposed_bbox[edge[1]] is None):
|
||||
point, _ = cls.intersect_edge_region_border(
|
||||
context.region, context.space_data, rv3d, bbox[edge[0]], bbox[edge[1]]
|
||||
)
|
||||
if point:
|
||||
new_bbox.append(point)
|
||||
if new_bbox:
|
||||
transposed_bbox = new_bbox
|
||||
|
||||
region = context.region
|
||||
borders = (0, region.width, 0, region.height)
|
||||
@@ -117,6 +140,96 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
return (obj, bbox_2d)
|
||||
return None
|
||||
|
||||
def intersect_edge_region_border(region, space, rv3d, v1, v2):
|
||||
def segment_intersect_near_plane(view_matrix, clip_start, p_world_a, p_world_b):
|
||||
a_view = view_matrix @ p_world_a
|
||||
b_view = view_matrix @ p_world_b
|
||||
z_near = -clip_start
|
||||
za = a_view.z
|
||||
zb = b_view.z
|
||||
denom = zb - za
|
||||
if denom == 0.0:
|
||||
return None, None
|
||||
t = (z_near - za) / denom
|
||||
if t < 0.0 or t > 1.0:
|
||||
return None, None
|
||||
p_view = a_view.lerp(b_view, t)
|
||||
cam_world = view_matrix.inverted()
|
||||
p_world = cam_world @ p_view
|
||||
return p_world, t
|
||||
|
||||
def is_inside_region(pt2d, region):
|
||||
return 0.0 <= pt2d.x <= region.width and 0.0 <= pt2d.y <= region.height
|
||||
|
||||
def clamp_to_region_border(point2d, region):
|
||||
x, y = point2d
|
||||
x_clamped = max(0.0, min(region.width, x))
|
||||
y_clamped = max(0.0, min(region.height, y))
|
||||
return Vector((x_clamped, y_clamped))
|
||||
|
||||
def find_nearby_onscreen_point(region, rv3d, p1, p2, initial_t_on_segment, max_iters=40, step=0.05):
|
||||
"""
|
||||
Use iterative approach: move t toward 0. Returns the first point that is inside region border
|
||||
"""
|
||||
t = initial_t_on_segment
|
||||
for i in range(max_iters):
|
||||
test_3d = p1.lerp(p2, t)
|
||||
test_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, test_3d)
|
||||
if test_2d is not None and is_inside_region(test_2d, region):
|
||||
return test_3d, test_2d, t
|
||||
# move t toward 0 by reducing it by a fraction of its current value
|
||||
t -= step
|
||||
# if t is already very small, break
|
||||
if t <= 1e-6:
|
||||
break
|
||||
|
||||
return None, None, None
|
||||
|
||||
# Ensures that all the calculation uses the same direction based on which point is on the screen
|
||||
if view3d_utils.location_3d_to_region_2d(region, rv3d, v1):
|
||||
onscreen_vert = v1
|
||||
offscreen_vert = v2
|
||||
else:
|
||||
onscreen_vert = v2
|
||||
offscreen_vert = v1
|
||||
# v2, v1 = v1, v2
|
||||
|
||||
clip_start = space.clip_start
|
||||
view_mat = rv3d.view_matrix
|
||||
inter_world, t_on_ab = segment_intersect_near_plane(view_mat, clip_start, onscreen_vert, offscreen_vert)
|
||||
|
||||
if inter_world is None:
|
||||
print("No intersection with viewport near plane found for the segment.")
|
||||
return
|
||||
|
||||
init_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, inter_world)
|
||||
|
||||
if init_2d is not None and is_inside_region(init_2d, region):
|
||||
final_world = inter_world
|
||||
final_2d = init_2d
|
||||
final_t = t_on_ab
|
||||
else:
|
||||
found_world, found_2d, found_t = find_nearby_onscreen_point(
|
||||
region, rv3d, onscreen_vert, offscreen_vert, t_on_ab, max_iters=600, step=0.01
|
||||
)
|
||||
if found_world is None:
|
||||
if init_2d is None:
|
||||
print("Initial projection invalid and iterative search failed.")
|
||||
return
|
||||
# fallback: clamp projected point to border via manual mapping
|
||||
final_2d = clamp_to_region_border(init_2d, region)
|
||||
final_world = None
|
||||
final_t = None
|
||||
# print("Iterative search failed; using clamped 2D:", final_2d)
|
||||
else:
|
||||
final_world = found_world
|
||||
final_2d = found_2d
|
||||
final_t = found_t
|
||||
# print(f"Found onscreen point at t={final_t:.4f}")
|
||||
|
||||
# print("Final 2D:", final_2d)
|
||||
return final_2d, v2
|
||||
|
||||
@classmethod
|
||||
def intersect_mouse_2d_bounding_box(cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float]):
|
||||
x, y = mouse_pos
|
||||
@@ -232,6 +345,161 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
@classmethod
|
||||
def ray_cast_by_proximity_2d(
|
||||
cls,
|
||||
context: bpy.types.Context,
|
||||
event: bpy.types.Event,
|
||||
snap_obj: SnapObj,
|
||||
):
|
||||
|
||||
def divide_vector(start, end, n):
|
||||
points = []
|
||||
delta = (end - start) / n
|
||||
for i in range(1, n):
|
||||
point = start + i * delta
|
||||
points.append(point)
|
||||
return points
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
mouse_pos = event.mouse_region_x, event.mouse_region_y
|
||||
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
|
||||
points = []
|
||||
|
||||
try:
|
||||
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
|
||||
except:
|
||||
loc = Vector((0, 0, 0))
|
||||
|
||||
verts_2d = [
|
||||
view3d_utils.location_3d_to_region_2d(region, rv3d, v) for v in snap_obj.verts_3d
|
||||
] # Numpy version is worst in performance
|
||||
|
||||
intersected = snap_obj.raycast_boxes(
|
||||
context, event, snap_obj.root, intersected=[], rays=(ray_origin, ray_direction)
|
||||
)
|
||||
edges = []
|
||||
for it in intersected:
|
||||
edges.extend(it.edges)
|
||||
edges = set(edges)
|
||||
|
||||
edge_verts = {}
|
||||
for e in edges:
|
||||
verts_idx = tuple(snap_obj.obj.data.edges[e].vertices)
|
||||
verts = snap_obj.obj.data.vertices
|
||||
v1 = snap_obj.obj.matrix_world @ verts[verts_idx[0]].co
|
||||
v1_2d = verts_2d[verts_idx[0]]
|
||||
v2 = snap_obj.obj.matrix_world @ verts[verts_idx[1]].co
|
||||
v2_2d = verts_2d[verts_idx[1]]
|
||||
if (v1_2d is None) ^ (v2_2d is None):
|
||||
point, _ = cls.intersect_edge_region_border(region, context.space_data, rv3d, v1, v2)
|
||||
if v1_2d is None:
|
||||
edge_verts[e] = (point, v2_2d)
|
||||
else:
|
||||
edge_verts[e] = (v1_2d, point)
|
||||
else:
|
||||
edge_verts[e] = (v1_2d, v2_2d)
|
||||
|
||||
snap_threshold = 10.0
|
||||
|
||||
for i, point in enumerate(verts_2d):
|
||||
if not point:
|
||||
continue
|
||||
distance = (Vector(mouse_pos) - point).length
|
||||
if distance <= snap_threshold:
|
||||
snap_point = {
|
||||
"object": snap_obj.obj,
|
||||
"type": "Vertex",
|
||||
"point": snap_obj.verts_3d[i],
|
||||
"distance": distance / 10,
|
||||
}
|
||||
points.append(snap_point)
|
||||
|
||||
count = 0
|
||||
selected_edges = {}
|
||||
for e in edges:
|
||||
p0, p1 = edge_verts[e]
|
||||
p0x, p0y = p0
|
||||
p1x, p1y = p1
|
||||
px, py = mouse_pos
|
||||
|
||||
# segment vector = p1 - p0
|
||||
sx = p1x - p0x
|
||||
sy = p1y - p0y
|
||||
|
||||
# seg length squared
|
||||
seg_len_sq = sx * sx + sy * sy
|
||||
|
||||
if seg_len_sq == 0.0:
|
||||
# degenerate segment: return distance to p0
|
||||
dx = px - p0x
|
||||
dy = py - p0y
|
||||
dist = math.hypot(dx, dy)
|
||||
return dist, (p0x, p0y), 0.0
|
||||
|
||||
# project (p - p0) onto seg: t = dot(p-p0, seg) / |seg|^2
|
||||
apx = px - p0x
|
||||
apy = py - p0y
|
||||
t = (apx * sx + apy * sy) / seg_len_sq
|
||||
|
||||
# clamp to segment
|
||||
if t <= 0.0:
|
||||
t_clamped = 0.0
|
||||
cx, cy = p0x, p0y
|
||||
elif t >= 1.0:
|
||||
t_clamped = 1.0
|
||||
cx, cy = p1x, p1y
|
||||
else:
|
||||
t_clamped = t
|
||||
cx = p0x + sx * t_clamped
|
||||
cy = p0y + sy * t_clamped
|
||||
|
||||
dx = px - cx
|
||||
dy = py - cy
|
||||
dist = math.hypot(dx, dy)
|
||||
if dist <= snap_threshold:
|
||||
selected_edges[dist] = e
|
||||
|
||||
if selected_edges:
|
||||
min_dist = float("inf")
|
||||
for key in selected_edges:
|
||||
if key < min_dist:
|
||||
min_dist = key
|
||||
|
||||
idx = snap_obj.obj.data.edges[selected_edges[min_dist]].vertices
|
||||
edge_verts = (snap_obj.verts_3d[idx[0]], snap_obj.verts_3d[idx[1]])
|
||||
division_points = divide_vector(
|
||||
edge_verts[0], edge_verts[1], 2
|
||||
) # TODO Make it work for different divisions
|
||||
for division_point in division_points:
|
||||
intersection = tool.Cad.point_on_edge(division_point, (ray_target, loc))
|
||||
distance = (division_point - intersection).length
|
||||
if distance < snap_threshold:
|
||||
snap_point = {
|
||||
"object": snap_obj.obj,
|
||||
"type": "Edge Center",
|
||||
"point": division_point.copy(),
|
||||
"distance": distance,
|
||||
}
|
||||
points.append(snap_point)
|
||||
|
||||
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), edge_verts)
|
||||
if intersection[0]:
|
||||
if tool.Cad.is_point_on_edge(intersection[1], edge_verts):
|
||||
distance = (intersection[1] - intersection[0]).length
|
||||
if distance < snap_threshold:
|
||||
snap_point = {
|
||||
"object": snap_obj.obj,
|
||||
"type": "Edge",
|
||||
"point": intersection[1].copy(),
|
||||
"edge_verts": edge_verts,
|
||||
"distance": distance,
|
||||
}
|
||||
points.append(snap_point)
|
||||
|
||||
return points
|
||||
|
||||
@classmethod
|
||||
def ray_cast_by_proximity(
|
||||
cls,
|
||||
@@ -457,7 +725,8 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
if bbox_2d:
|
||||
if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d):
|
||||
if tool.Raycast.object_is_visible_in_clipping_plane(obj):
|
||||
objs_to_raycast.append(obj)
|
||||
snap_obj = cls.create_snap_obj(obj)
|
||||
objs_to_raycast.append(snap_obj)
|
||||
|
||||
return objs_to_raycast
|
||||
|
||||
@@ -474,12 +743,6 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
face_index = None
|
||||
# Wireframes
|
||||
if obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0):
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj)
|
||||
if snap_points:
|
||||
hit = sorted(snap_points, key=lambda x: x["distance"])[0]["point"]
|
||||
if hit:
|
||||
hit_world = obj.original.matrix_world @ hit
|
||||
return obj, hit_world, face_index
|
||||
return None, None, None
|
||||
# Meshes
|
||||
else:
|
||||
@@ -514,19 +777,20 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
|
||||
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
|
||||
|
||||
for obj in objs_to_raycast:
|
||||
for snap_obj in objs_to_raycast:
|
||||
if not include_wireframes and (
|
||||
obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0)
|
||||
snap_obj.obj.type in {"EMPTY", "CURVE"}
|
||||
or (hasattr(snap_obj.obj.data, "polygons") and len(snap_obj.obj.data.polygons) == 0)
|
||||
):
|
||||
continue
|
||||
|
||||
snap_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, obj)
|
||||
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj.obj)
|
||||
|
||||
if hit is not None:
|
||||
length_squared = (hit - ray_origin).length_squared
|
||||
if best_obj is None or length_squared < best_length_squared:
|
||||
best_length_squared = length_squared
|
||||
best_obj = snap_obj
|
||||
best_obj = hit_obj
|
||||
best_hit = hit
|
||||
best_face_index = face_index
|
||||
|
||||
@@ -536,6 +800,79 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
@classmethod
|
||||
def ray_cast_and_get_closest_to_camera_snaps(
|
||||
cls,
|
||||
context: bpy.types.Context,
|
||||
event: bpy.types.Event,
|
||||
objs_to_raycast: list[bpy.types.Object],
|
||||
) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]:
|
||||
closest_length_squared = 1.0
|
||||
closest_obj = None
|
||||
closest_hit = None
|
||||
closest_face_index = None
|
||||
|
||||
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
|
||||
|
||||
closest_snaps = []
|
||||
hit = None
|
||||
|
||||
for snap_obj in objs_to_raycast:
|
||||
if snap_obj.obj.type in {"EMPTY", "CURVE"} or (
|
||||
hasattr(snap_obj.obj.data, "polygons") and len(snap_obj.obj.data.polygons) == 0
|
||||
):
|
||||
# For wireframe objects we have to test all the snaps to see which is closer
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj)
|
||||
closest_wf_hit = None
|
||||
closest_wf_length_squared = 1.0
|
||||
closest_wf_point = None
|
||||
if snap_points:
|
||||
for point in snap_points:
|
||||
point["group"] = "Wireframe"
|
||||
closest_snaps.append(point)
|
||||
length = (point["point"] - ray_origin).length_squared
|
||||
if closest_wf_hit is None or length < closest_wf_length_squared:
|
||||
closest_wf_length_squared = length
|
||||
closest_wf_hit = point["point"]
|
||||
closest_wf_point = point
|
||||
|
||||
if closest_wf_point:
|
||||
hit_obj = closest_wf_point["object"]
|
||||
hit = closest_wf_point["point"]
|
||||
face_index = None
|
||||
|
||||
else:
|
||||
# Solid objects
|
||||
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj.obj)
|
||||
|
||||
if hit:
|
||||
snap_point = {
|
||||
"point": hit,
|
||||
"type": "Face",
|
||||
"group": "Object",
|
||||
"object": hit_obj,
|
||||
"face_index": face_index,
|
||||
"distance": 9, # High value so it has low priority
|
||||
}
|
||||
closest_snaps.append(snap_point)
|
||||
|
||||
# Here we test which is closer, including wireframe and solid objects
|
||||
if hit is not None:
|
||||
length_squared = (hit - ray_origin).length_squared
|
||||
if closest_obj is None or length_squared < closest_length_squared:
|
||||
closest_length_squared = length_squared
|
||||
closest_obj = hit_obj
|
||||
closest_hit = hit
|
||||
closest_face_index = face_index
|
||||
|
||||
# Label snaps from the closest object
|
||||
if closest_obj is not None:
|
||||
for snap in closest_snaps:
|
||||
if snap["object"] == closest_obj:
|
||||
snap["is_closest_to_camera"] = True
|
||||
|
||||
return closest_snaps
|
||||
|
||||
@classmethod
|
||||
def calculate_snap_threshold(cls, view_distance):
|
||||
snap_threshold = view_distance / 100
|
||||
@@ -547,3 +884,260 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
if lens < 50:
|
||||
snap_threshold *= value
|
||||
return snap_threshold
|
||||
|
||||
@classmethod
|
||||
def create_snap_obj(cls, obj):
|
||||
for snap_obj in cls.snap_objs:
|
||||
if obj.name == snap_obj.obj.name:
|
||||
return snap_obj
|
||||
snap_obj = SnapObj(obj)
|
||||
cls.snap_objs.append(snap_obj)
|
||||
return snap_obj
|
||||
|
||||
@classmethod
|
||||
def clear_snap_objs(cls):
|
||||
TreeNode.__clear_all__()
|
||||
SnapObj.__clear_all__()
|
||||
cls.snap_objs.clear()
|
||||
|
||||
|
||||
class TreeNode:
|
||||
all = []
|
||||
|
||||
def __init__(self, box: tuple):
|
||||
self.__class__.all.append(self)
|
||||
self.box = box
|
||||
self.child_a = None
|
||||
self.child_b = None
|
||||
self.edges = []
|
||||
|
||||
def __clear_all__():
|
||||
for instance in TreeNode.all:
|
||||
del instance
|
||||
TreeNode.all.clear()
|
||||
|
||||
|
||||
class SnapObj:
|
||||
max_depth = 9
|
||||
all = []
|
||||
|
||||
def __init__(self, obj: bpy.types.Object):
|
||||
self.__class__.all.append(self)
|
||||
self.obj = obj
|
||||
self.root = self._create_root_node()
|
||||
self.root.edges = [e.index for e in obj.data.edges]
|
||||
self.split_box(self.root, 0)
|
||||
self.verts_3d = [obj.matrix_world @ v.co for v in obj.data.vertices]
|
||||
self.snap_points = []
|
||||
|
||||
def __clear_all__():
|
||||
for instance in SnapObj.all:
|
||||
del instance
|
||||
SnapObj.all.clear()
|
||||
|
||||
def _create_root_node(self) -> TreeNode:
|
||||
bbox = tool.Blender.get_object_bounding_box(self.obj)
|
||||
min_point = self.obj.matrix_world @ bbox["min_point"]
|
||||
max_point = self.obj.matrix_world @ bbox["max_point"]
|
||||
new_bbox = self.expand_bounding_box((min_point, max_point))
|
||||
return TreeNode(new_bbox)
|
||||
|
||||
def divide_bounding_box_along_longest_axis(
|
||||
self, min_pt: Vector, max_pt: Vector
|
||||
) -> Union[tuple[Vector, Vector], tuple[Vector, Vector]]:
|
||||
"""
|
||||
Divide a bounding box into two equal parts along the axis with the longest dimension.
|
||||
|
||||
Args:
|
||||
min_pt: The minimum point of the bounding box.
|
||||
max_pt: The maximum point of the bounding box.
|
||||
|
||||
Returns:
|
||||
list: A list of two tuples, each containing the minimum and maximum points of the divided boxes.
|
||||
"""
|
||||
|
||||
# Calculate the dimensions of the box
|
||||
dx = max_pt.x - min_pt.x
|
||||
dy = max_pt.y - min_pt.y
|
||||
dz = max_pt.z - min_pt.z
|
||||
|
||||
# Determine the axis with the longest dimension
|
||||
if dx >= dy and dx >= dz:
|
||||
# Divide along the x-axis
|
||||
mid_x = min_pt.x + dx / 2
|
||||
box1 = (min_pt, Vector((mid_x, max_pt.y, max_pt.z)))
|
||||
box2 = (Vector((mid_x, min_pt.y, min_pt.z)), max_pt)
|
||||
elif dy >= dx and dy >= dz:
|
||||
# Divide along the y-axis
|
||||
mid_y = min_pt.y + dy / 2
|
||||
box1 = (min_pt, Vector((max_pt.x, mid_y, max_pt.z)))
|
||||
box2 = (Vector((min_pt.x, mid_y, min_pt.z)), max_pt)
|
||||
else:
|
||||
# Divide along the z-axis
|
||||
mid_z = min_pt.z + dz / 2
|
||||
box1 = (min_pt, Vector((max_pt.x, max_pt.y, mid_z)))
|
||||
box2 = (Vector((min_pt.x, min_pt.y, mid_z)), max_pt)
|
||||
|
||||
return [box1, box2]
|
||||
|
||||
def expand_bounding_box(self, box: tuple[Vector, Vector], offset: float = 0.1) -> tuple[Vector, Vector]:
|
||||
"""
|
||||
Expand a 3D bounding box by a given offset.
|
||||
|
||||
Args:
|
||||
min_pt: The minimum point of the bounding box.
|
||||
max_pt: The maximum point of the bounding box.
|
||||
offset: The offset to expand the bounding box by.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the new minimum and maximum points of the expanded bounding box.
|
||||
"""
|
||||
|
||||
min_pt, max_pt = box
|
||||
# Calculate the new minimum and maximum points
|
||||
new_min_pt = Vector((min_pt.x - offset, min_pt.y - offset, min_pt.z - offset))
|
||||
new_max_pt = Vector((max_pt.x + offset, max_pt.y + offset, max_pt.z + offset))
|
||||
|
||||
return new_min_pt, new_max_pt
|
||||
|
||||
def split_box(self, parent: TreeNode, depth: int):
|
||||
"""
|
||||
Splits the bounding box creating two child nodes to compose a BVH Tree recursively.
|
||||
|
||||
Args:
|
||||
parent: the TreeNode instance that represents the parent node of a BVH Tree.
|
||||
depth: the depth of the BVH Tree no be used in recursion.
|
||||
"""
|
||||
if depth > self.max_depth:
|
||||
return
|
||||
box_a, box_b = self.divide_bounding_box_along_longest_axis(parent.box[0], parent.box[1])
|
||||
parent.child_a = TreeNode(box_a)
|
||||
parent.child_b = TreeNode(box_b)
|
||||
edges_a = []
|
||||
edges_b = []
|
||||
for e in parent.edges:
|
||||
verts_idx = [v for v in self.obj.data.edges[e].vertices]
|
||||
verts_coords = []
|
||||
for idx in verts_idx:
|
||||
if idx < len(self.obj.data.vertices):
|
||||
verts_coords.append(self.obj.matrix_world @ self.obj.data.vertices[idx].co)
|
||||
if self.line_intersects_box(verts_coords[0], verts_coords[1], parent.child_a.box):
|
||||
edges_a.append(e)
|
||||
if self.line_intersects_box(verts_coords[0], verts_coords[1], parent.child_b.box):
|
||||
edges_b.append(e)
|
||||
parent.child_a.edges = edges_a
|
||||
parent.child_b.edges = edges_b
|
||||
self.split_box(parent.child_a, depth + 1)
|
||||
self.split_box(parent.child_b, depth + 1)
|
||||
|
||||
def raycast_box(
|
||||
self, context: bpy.types.Context, event: bpy.types.Event, node: TreeNode, rays: tuple[Vector, Vector]
|
||||
) -> bool:
|
||||
"""
|
||||
Raycast bounding box.
|
||||
|
||||
Args:
|
||||
context: Blender context.
|
||||
event: Blender event.
|
||||
node: a TreeNode instance.
|
||||
rays: tuple containing ray origin and ray direction
|
||||
|
||||
Returns:
|
||||
True if hits the box or False otherwise.
|
||||
"""
|
||||
box = node.box
|
||||
min_v = box[0]
|
||||
max_v = box[1]
|
||||
t_min = 0.0
|
||||
t_max = float("inf")
|
||||
ray_origin, ray_dir = rays
|
||||
inv_dir = Vector((1.0 / r if r != 0.0 else 1e32) for r in (ray_dir.x, ray_dir.y, ray_dir.z))
|
||||
# X
|
||||
tx1 = (min_v.x - ray_origin.x) * inv_dir[0]
|
||||
tx2 = (max_v.x - ray_origin.x) * inv_dir[0]
|
||||
tmin = min(tx1, tx2)
|
||||
tmax = max(tx1, tx2)
|
||||
# Y
|
||||
ty1 = (min_v.y - ray_origin.y) * inv_dir[1]
|
||||
ty2 = (max_v.y - ray_origin.y) * inv_dir[1]
|
||||
tmin = max(tmin, min(ty1, ty2))
|
||||
tmax = min(tmax, max(ty1, ty2))
|
||||
# Z
|
||||
tz1 = (min_v.z - ray_origin.z) * inv_dir[2]
|
||||
tz2 = (max_v.z - ray_origin.z) * inv_dir[2]
|
||||
tmin = max(tmin, min(tz1, tz2))
|
||||
tmax = min(tmax, max(tz1, tz2))
|
||||
return (tmax >= max(tmin, t_min)) and (tmin <= t_max)
|
||||
|
||||
def line_intersects_box(self, v1: mathutils.Vector, v2: mathutils.Vector, box: tuple) -> bool:
|
||||
"""
|
||||
Check if a line segment intersects an axis-aligned bounding box (AABB).
|
||||
|
||||
Args:
|
||||
v1: The first endpoint of the line segment as a mathutils.Vector.
|
||||
v2: The second endpoint of the line segment as a mathutils.Vector.
|
||||
box: A tuple containing the minimum and maximum points of the AABB, where each point is a mathutils.Vector.
|
||||
|
||||
Returns:
|
||||
bool: True if the segment [v1, v2] intersects the AABB; otherwise, False.
|
||||
"""
|
||||
bmin, bmax = box
|
||||
dir = v2 - v1
|
||||
tmin = 0.0
|
||||
tmax = 1.0
|
||||
|
||||
for i in range(3):
|
||||
if abs(dir[i]) < 1e-12:
|
||||
# Line is parallel to slab. If origin not within slab -> no hit.
|
||||
if v1[i] < bmin[i] or v1[i] > bmax[i]:
|
||||
return False
|
||||
else:
|
||||
ood = 1.0 / dir[i]
|
||||
t1 = (bmin[i] - v1[i]) * ood
|
||||
t2 = (bmax[i] - v1[i]) * ood
|
||||
if t1 > t2:
|
||||
t1, t2 = t2, t1
|
||||
if t1 > tmin:
|
||||
tmin = t1
|
||||
if t2 < tmax:
|
||||
tmax = t2
|
||||
if tmin > tmax:
|
||||
return False
|
||||
|
||||
# If any overlap in [0,1] exists, there's intersection
|
||||
return (tmax >= 0.0) and (tmin <= 1.0)
|
||||
|
||||
def raycast_boxes(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
event: bpy.Types.Event,
|
||||
node: TreeNode,
|
||||
intersected: Union[TreeNode] = [],
|
||||
rays: tuple[Vector, Vector] = (),
|
||||
) -> Union[TreeNode]:
|
||||
"""
|
||||
Raycast bounding box subdivisions recursively.
|
||||
|
||||
Args:
|
||||
context: Blender context.
|
||||
event: Blender event.
|
||||
node: a TreeNode instance.
|
||||
intersected: list of intersected boxes to use in recursion.
|
||||
rays: tuple containing ray origin and ray direction
|
||||
|
||||
Returns:
|
||||
tuple: a list of TreeNode instances that represent the subdivided boxes hit by the ray cast.
|
||||
"""
|
||||
if not node.child_a:
|
||||
intersected.append(node)
|
||||
return intersected
|
||||
|
||||
intersects_a = self.raycast_box(context, event, node.child_a, rays)
|
||||
intersects_b = self.raycast_box(context, event, node.child_b, rays)
|
||||
if intersects_a:
|
||||
intersected = self.raycast_boxes(context, event, node.child_a, intersected, rays)
|
||||
|
||||
if intersects_b:
|
||||
intersected = self.raycast_boxes(context, event, node.child_b, intersected, rays)
|
||||
|
||||
return intersected
|
||||
|
||||
@@ -93,38 +93,16 @@ class Root(bonsai.core.tool.Root):
|
||||
elif dest.is_a("IfcTypeProduct"):
|
||||
if not source.RepresentationMaps:
|
||||
return copied_entities
|
||||
|
||||
# Copy representation maps while preserving mapped representation structures
|
||||
new_maps = []
|
||||
for i, rep_map in enumerate(source.RepresentationMaps):
|
||||
source_rep = rep_map.MappedRepresentation
|
||||
|
||||
# Copy the map itself
|
||||
new_map = ifcopenshell.util.element.copy(tool.Ifc.get(), rep_map)
|
||||
|
||||
# Handle the mapped representation - preserve mapping structure if present
|
||||
if (
|
||||
source_rep.RepresentationType == "MappedRepresentation"
|
||||
and len(source_rep.Items) == 1
|
||||
and source_rep.Items[0].is_a("IfcMappedItem")
|
||||
):
|
||||
# This is a mapped representation - preserve the structure
|
||||
new_rep = ifcopenshell.util.element.copy(tool.Ifc.get(), source_rep)
|
||||
new_rep.Items = [ifcopenshell.util.element.copy(tool.Ifc.get(), item) for item in source_rep.Items]
|
||||
new_map.MappedRepresentation = new_rep
|
||||
else:
|
||||
# Not a mapped representation - use copy_deep as before
|
||||
new_map.MappedRepresentation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
source_rep,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
copied_entities=copied_entities,
|
||||
)
|
||||
|
||||
new_maps.append(new_map)
|
||||
|
||||
dest.RepresentationMaps = new_maps
|
||||
dest.RepresentationMaps = [
|
||||
ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
m,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
copied_entities=copied_entities,
|
||||
)
|
||||
for m in source.RepresentationMaps
|
||||
]
|
||||
return copied_entities
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -388,57 +388,36 @@ class Snap(bonsai.core.tool.Snap):
|
||||
|
||||
# Objects
|
||||
objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, objs_2d_bbox)
|
||||
# Wireframes
|
||||
# For wireframe we have to get all the objects so we can further calculate edge intersection
|
||||
for snap_obj in objs_to_raycast:
|
||||
if snap_obj.type in {"EMPTY", "CURVE"} or (snap_obj.type == "MESH" and len(snap_obj.data.polygons) == 0):
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj)
|
||||
if snap_points:
|
||||
for point in snap_points:
|
||||
point["group"] = "Wireframe"
|
||||
detected_snaps.append(point)
|
||||
closest_snaps = tool.Raycast.ray_cast_and_get_closest_to_camera_snaps(context, event, objs_to_raycast)
|
||||
detected_snaps.extend(closest_snaps)
|
||||
|
||||
if (space.shading.type == "SOLID" and space.shading.show_xray) or (
|
||||
xray_mode = (space.shading.type == "SOLID" and space.shading.show_xray) or (
|
||||
space.shading.type == "WIREFRAME" and space.shading.show_xray_wireframe
|
||||
):
|
||||
results = []
|
||||
for obj in objs_to_raycast:
|
||||
results.append(tool.Raycast.cast_rays_to_single_object(context, event, obj))
|
||||
else:
|
||||
results = []
|
||||
results.append(tool.Raycast.cast_rays_and_get_best_object(context, event, objs_to_raycast))
|
||||
)
|
||||
|
||||
for result in results:
|
||||
snap_obj = result[0]
|
||||
hit = result[1]
|
||||
face_index = result[2]
|
||||
if hit is not None:
|
||||
# Wireframes
|
||||
if snap_obj.type in {"EMPTY", "CURVE"} or (
|
||||
snap_obj.type == "MESH" and len(snap_obj.data.polygons) == 0
|
||||
):
|
||||
continue
|
||||
# Meshes
|
||||
else:
|
||||
# Add face snap
|
||||
snap_point = {
|
||||
"point": hit,
|
||||
"type": "Face",
|
||||
"group": "Object",
|
||||
"object": snap_obj,
|
||||
"face_index": face_index,
|
||||
"distance": 9, # High value so it has low priority
|
||||
}
|
||||
detected_snaps.append(snap_point)
|
||||
|
||||
# Add vertex and edge snap
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity(
|
||||
context, event, snap_obj, snap_obj.data.polygons[face_index]
|
||||
)
|
||||
if snap_points:
|
||||
for point in snap_points:
|
||||
point["group"] = "Object"
|
||||
detected_snaps.append(point)
|
||||
for snap_obj in objs_to_raycast:
|
||||
for snap in closest_snaps:
|
||||
if snap_obj.obj == snap["object"]:
|
||||
if xray_mode:
|
||||
if "face_index" in snap and snap["face_index"] is not None:
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj)
|
||||
for point in snap_points:
|
||||
point["group"] = "Object"
|
||||
detected_snaps.append(point)
|
||||
else:
|
||||
# If it is a solid object that is closest to camera it ignores all the rest
|
||||
if (
|
||||
"is_closest_to_camera" in snap
|
||||
and snap["is_closest_to_camera"]
|
||||
and snap["group"] == "Object"
|
||||
):
|
||||
closest_snap = [snap] # discards objects that aren't the closest
|
||||
if "face_index" in snap and snap["face_index"] is not None:
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj)
|
||||
for point in snap_points:
|
||||
point["group"] = "Object"
|
||||
closest_snap.append(point)
|
||||
detected_snaps = closest_snap
|
||||
|
||||
# snap to cut geometry (e.g. in plan view)
|
||||
if CutDecorator.installed:
|
||||
|
||||
+121
-152
@@ -21,13 +21,13 @@ from __future__ import annotations
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from collections.abc import Generator, Iterable
|
||||
from math import pi
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.attribute
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.api.type
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.classification
|
||||
@@ -991,114 +991,104 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
return poly
|
||||
|
||||
@classmethod
|
||||
def get_bmesh_from_polygon(cls, poly: Polygon, h: float, polygon_is_si: bool = False) -> bmesh.types.BMesh:
|
||||
"""
|
||||
:param h: Height, in meters.
|
||||
:param polygon_is_si: Should be True if `poly` is defined in meters.
|
||||
"""
|
||||
mat = Matrix()
|
||||
bm = bmesh.new()
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
mat_invert = mat.inverted()
|
||||
si_conversion = 1.0 if polygon_is_si else ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
new_verts = [
|
||||
# bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion)) for v in poly.exterior.coords[0:-1]
|
||||
bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion))
|
||||
for v in shapely.get_exterior_ring(poly).coords[0:-1]
|
||||
]
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
|
||||
bmesh.ops.triangle_fill(bm, edges=bm.edges)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
if h != 0:
|
||||
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
|
||||
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
|
||||
return bm
|
||||
|
||||
@classmethod
|
||||
def get_named_obj_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Object:
|
||||
mesh = cls.get_named_mesh_from_bmesh(name, bmesh)
|
||||
obj = cls.get_named_obj_from_mesh(name, mesh)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_named_obj_from_mesh(cls, name: str, mesh: bpy.types.Mesh) -> bpy.types.Object:
|
||||
def create_object(cls, name: str) -> bpy.types.Object:
|
||||
mesh = bpy.data.meshes.new(name=name)
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_named_mesh_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Mesh:
|
||||
mesh = bpy.data.meshes.new(name=name)
|
||||
bmesh.to_mesh(mesh)
|
||||
bmesh.free()
|
||||
return mesh
|
||||
def set_obj_origin_to_polygon_center(cls, obj: bpy.types.Object, poly: Polygon, polygon_is_si: bool = True) -> None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
centroid = poly.centroid
|
||||
if polygon_is_si:
|
||||
obj.location = Vector((centroid.x, centroid.y, 0))
|
||||
else:
|
||||
obj.location = Vector((centroid.x * unit_scale, centroid.y * unit_scale, 0))
|
||||
|
||||
@classmethod
|
||||
def get_transformed_mesh_from_local_to_global(cls, mesh: bpy.types.Mesh) -> bpy.types.Mesh:
|
||||
active_obj = cls.get_active_obj()
|
||||
mat = active_obj.matrix_world
|
||||
mesh.transform(mat.inverted())
|
||||
mesh.update()
|
||||
return mesh
|
||||
def get_2d_vertices_from_polygon(
|
||||
cls,
|
||||
poly: Polygon,
|
||||
obj: bpy.types.Object,
|
||||
polygon_is_si: bool = True,
|
||||
) -> list[list[float]]:
|
||||
"""Convert a world-space shapely polygon to 2D vertices in obj's local space, in IFC file units.
|
||||
|
||||
:param poly: The polygon in world space.
|
||||
:param obj: The Blender object whose local space is used.
|
||||
:param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
|
||||
:return: List of [x, y] coordinates (not closed).
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
bpy.context.view_layer.update()
|
||||
mat_inv = obj.matrix_world.inverted()
|
||||
coords_2d = []
|
||||
for v in shapely.get_exterior_ring(poly).coords[:-1]:
|
||||
world_si = Vector((v[0], v[1], 0))
|
||||
if not polygon_is_si:
|
||||
world_si = world_si * unit_scale
|
||||
local_si = mat_inv @ world_si
|
||||
coords_2d.append([local_si.x / unit_scale, local_si.y / unit_scale])
|
||||
return coords_2d
|
||||
|
||||
@classmethod
|
||||
def edit_active_space_obj_from_mesh(cls, mesh: bpy.types.Mesh) -> None:
|
||||
active_obj = bpy.context.active_object
|
||||
old_mesh = active_obj.data
|
||||
old_mesh_name = old_mesh.name
|
||||
assert active_obj and isinstance(old_mesh, bpy.types.Mesh)
|
||||
tool.Geometry.get_mesh_props(mesh).ifc_definition_id = tool.Geometry.get_mesh_props(old_mesh).ifc_definition_id
|
||||
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
|
||||
tool.Ifc.edit(active_obj)
|
||||
tool.Blender.remove_data_block(old_mesh)
|
||||
# Rename after old mesh is removed to avoid .001 suffix.
|
||||
mesh.name = old_mesh_name
|
||||
def set_extrusion_representation_from_polygon(
|
||||
cls,
|
||||
obj: bpy.types.Object,
|
||||
element: ifcopenshell.entity_instance,
|
||||
poly: Polygon,
|
||||
depth_ifc: float,
|
||||
polygon_is_si: bool = True,
|
||||
) -> None:
|
||||
"""Create or replace the IFC body representation from a polygon extrusion.
|
||||
|
||||
:param obj: The Blender object.
|
||||
:param element: The IFC product entity.
|
||||
:param poly: The polygon in world space.
|
||||
:param depth_ifc: The extrusion depth in IFC file units.
|
||||
:param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
|
||||
|
||||
coords_2d = cls.get_2d_vertices_from_polygon(poly, obj, polygon_is_si)
|
||||
|
||||
curve = builder.polyline(coords_2d, closed=True)
|
||||
item = builder.extrude(curve, magnitude=depth_ifc)
|
||||
|
||||
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if old_body:
|
||||
context = old_body.ContextOfItems
|
||||
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=old_body)
|
||||
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body)
|
||||
else:
|
||||
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
new_body = builder.get_representation(context, item)
|
||||
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_body,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def set_obj_origin_to_bboxcenter(cls, obj: bpy.types.Object) -> None:
|
||||
mat = obj.matrix_world
|
||||
inverted = mat.inverted()
|
||||
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
|
||||
global_bbox_center = mat @ local_bbox_center
|
||||
def set_space_representation_from_polygon(
|
||||
cls,
|
||||
obj: bpy.types.Object,
|
||||
element: ifcopenshell.entity_instance,
|
||||
poly: Polygon,
|
||||
h: float,
|
||||
polygon_is_si: bool = True,
|
||||
) -> None:
|
||||
"""Create or replace the IFC body representation of a space from a polygon.
|
||||
|
||||
oldLoc = obj.location
|
||||
newLoc = global_bbox_center
|
||||
diff = newLoc - oldLoc
|
||||
for vert in obj.data.vertices:
|
||||
aux_vector = mat @ vert.co
|
||||
aux_vector = aux_vector - diff
|
||||
vert.co = inverted @ aux_vector
|
||||
obj.location = newLoc
|
||||
|
||||
@classmethod
|
||||
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
|
||||
mat = obj.matrix_world
|
||||
inverted = mat.inverted()
|
||||
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
|
||||
global_bbox_center = mat @ local_bbox_center
|
||||
global_obj_origin = global_bbox_center
|
||||
global_obj_origin.z = 0
|
||||
|
||||
oldLoc = obj.location
|
||||
newLoc = global_obj_origin
|
||||
diff = newLoc - oldLoc
|
||||
for vert in obj.data.vertices:
|
||||
aux_vector = mat @ vert.co
|
||||
aux_vector = aux_vector - diff
|
||||
vert.co = inverted @ aux_vector
|
||||
obj.location = newLoc
|
||||
:param h: The height in SI (meters).
|
||||
"""
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
|
||||
@classmethod
|
||||
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
|
||||
@@ -1145,65 +1135,53 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
if z != 0:
|
||||
obj.location = obj.location + Vector((0, 0, z))
|
||||
|
||||
@classmethod
|
||||
def get_2d_vertices_from_obj(cls, obj: bpy.types.Object) -> list[tuple]:
|
||||
points = []
|
||||
vectors = [v.co for v in obj.data.vertices.values()]
|
||||
for vector in vectors:
|
||||
points.append(vector.xy)
|
||||
|
||||
points.append(vectors[0].xy)
|
||||
return points
|
||||
|
||||
@classmethod
|
||||
def get_scaled_2d_vertices(cls, points: list[Vector]) -> list[tuple[float, float]]:
|
||||
model = tool.Ifc.get()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(model)
|
||||
_points = []
|
||||
for p in points:
|
||||
_p = list(p)
|
||||
_p[0] /= unit_scale
|
||||
_p[1] /= unit_scale
|
||||
_points.append(_p)
|
||||
return _points
|
||||
|
||||
@classmethod
|
||||
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj: bpy.types.Object, vertices: list[Vector]) -> None:
|
||||
body = cls.get_body_representation(obj)
|
||||
model = tool.Ifc.get()
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
area = extrusion.SweptArea
|
||||
old_area = area.OuterCurve
|
||||
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(model)
|
||||
outer_curve = builder.polyline(vertices, closed=True)
|
||||
|
||||
area.OuterCurve = outer_curve
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_area)
|
||||
|
||||
@classmethod
|
||||
def get_body_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
return ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
@classmethod
|
||||
def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None:
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
|
||||
bonsai.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Root,
|
||||
obj=obj,
|
||||
ifc_class="IfcSpace",
|
||||
should_add_representation=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def assign_type_to_obj(cls, obj: bpy.types.Object) -> None:
|
||||
# TODO this code looks in the wrong spot and suspicious
|
||||
props = tool.Model.get_model_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
relating_type_id = props.relating_type_id
|
||||
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
relating_type = ifc_file.by_id(int(relating_type_id))
|
||||
ifc_class = relating_type.is_a()
|
||||
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, ifc_file.schema)[0]
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
|
||||
bonsai.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Root,
|
||||
obj=obj,
|
||||
ifc_class=instance_class,
|
||||
should_add_representation=False,
|
||||
)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
ifcopenshell.api.type.assign_type(ifc_file, related_objects=[element], relating_type=relating_type)
|
||||
|
||||
@classmethod
|
||||
def set_covering_representation_from_polygon(
|
||||
cls,
|
||||
obj: bpy.types.Object,
|
||||
poly: Polygon,
|
||||
polygon_is_si: bool = True,
|
||||
) -> None:
|
||||
"""Create the covering body representation from a polygon, extruded by the type's material layer thickness."""
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
relating_type = ifcopenshell.util.element.get_type(element)
|
||||
material = ifcopenshell.util.element.get_material(relating_type, should_skip_usage=True)
|
||||
depth = 0.0
|
||||
if material and material.is_a("IfcMaterialLayerSet"):
|
||||
depth = sum(layer.LayerThickness for layer in material.MaterialLayers)
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, depth, polygon_is_si)
|
||||
|
||||
@classmethod
|
||||
def assign_relating_type_to_element(
|
||||
cls,
|
||||
@@ -1214,15 +1192,6 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
) -> None:
|
||||
bonsai.core.type.assign_type(ifc, tool.Model, type, element=element, type=relating_type)
|
||||
|
||||
@classmethod
|
||||
def regen_obj_representation(cls, obj: bpy.types.Object, body: ifcopenshell.entity_instance) -> None:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def set_space_visibility(cls, is_visible: bool) -> None:
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
|
||||
@@ -75,16 +75,7 @@ class Type(bonsai.core.tool.Type):
|
||||
|
||||
@classmethod
|
||||
def get_model_types(cls) -> list[ifcopenshell.entity_instance]:
|
||||
ifc_file = tool.Ifc.get()
|
||||
types = ifc_file.by_type("IfcElementType")
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
types += ifc_file.by_type("IfcWindowStyle")
|
||||
types += ifc_file.by_type("IfcDoorStyle")
|
||||
types += ifc_file.by_type("IfcSpatialStructureElementType")
|
||||
else:
|
||||
types += ifc_file.by_type("IfcSpatialElementType")
|
||||
types += ifc_file.by_type("IfcTypeProduct", include_subtypes=False)
|
||||
return types
|
||||
return tool.Ifc.get().by_type("IfcTypeProduct")
|
||||
|
||||
@classmethod
|
||||
def get_object_data(cls, obj: bpy.types.Object) -> Union[bpy.types.ID, None]:
|
||||
|
||||
@@ -19,4 +19,5 @@ This chapter covers how you can help contribute to Bonsai.
|
||||
undo_system
|
||||
writing_docs
|
||||
debugging
|
||||
maintenance
|
||||
ide/index
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
Maintenance
|
||||
===========
|
||||
|
||||
This page documents what needs to be updated in various maintenance scenarios.
|
||||
|
||||
Python Version Added or Removed
|
||||
--------------------------------
|
||||
|
||||
When adding or removing a supported Python version, update the following:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - File
|
||||
- What to update
|
||||
* - ``.github/workflows/ci-black-formatting.yaml``
|
||||
- ``MIN_IOS_PY_VERSION``
|
||||
* - ``.github/workflows/ci-ifcopenshell-python-pypi.yml``
|
||||
- ``pyver`` matrix
|
||||
* - ``.github/workflows/ci-ifcopenshell-python.yml``
|
||||
- ``pyver`` matrix
|
||||
* - ``nix/build-all.py``
|
||||
- ``PYTHON_VERSIONS`` list
|
||||
* - ``src/bsdd/pyproject.toml``
|
||||
- ``requires-python``
|
||||
* - ``src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst``
|
||||
- add or remove the row in the ZIP packages table
|
||||
* - ``src/ifcopenshell-python/Makefile``
|
||||
- ``SUPPORTED_PYVERSIONS``
|
||||
* - ``src/ifcopenshell-python/pyproject.toml``
|
||||
- ``requires-python``
|
||||
* - ``src/ifcopenshell-python/test/test_package.py``
|
||||
- ``SUPPORTED_PY_VERSIONS`` tuple
|
||||
* - ``win/build-all-win.py``
|
||||
- ``PYTHON_VERSIONS`` list
|
||||
|
||||
Blender Version Updated
|
||||
-----------------------
|
||||
|
||||
When a new Blender version is released and supported:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - File
|
||||
- What to update
|
||||
* - ``.github/workflows/ci-bonsai-daily.yml``
|
||||
- Blender download URL
|
||||
|
||||
Blender's Bundled Python Version Updated
|
||||
-----------------------------------------
|
||||
|
||||
When Blender ships with a new Python version:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - File
|
||||
- What to update
|
||||
* - ``.github/workflows/ci-black-formatting.yaml``
|
||||
- ``MIN_BLENDER_PY_VERSION``
|
||||
* - ``src/bonsai/Makefile``
|
||||
- ``SUPPORTED_PYVERSIONS``
|
||||
* - ``src/bonsai/scripts/dev_environment.py``
|
||||
- ``PYTHON_VERSION`` mapping (Blender version, bundled Python version)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Clone or update Bonsai external dependencies.
|
||||
|
||||
Must be run from the repository root.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
DEPS = [
|
||||
("https://projects.blender.org/pioverfour/sun_position.git", "sun_position"),
|
||||
("https://github.com/kevancress/MeasureIt_ARCH", "MeasureIt_ARCH"),
|
||||
("https://github.com/nortikin/sverchok.git", "sverchok"),
|
||||
]
|
||||
|
||||
base = Path("src/bonsai/external_dependencies")
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for url, name in DEPS:
|
||||
path = base / name
|
||||
if not path.exists():
|
||||
subprocess.check_call(["git", "clone", url, str(path)])
|
||||
else:
|
||||
subprocess.check_call(["git", "-C", str(path), "pull", "--rebase"])
|
||||
@@ -273,10 +273,10 @@ if BPY_IS_LOADED:
|
||||
f"Couldn't find locale path in the source directory, creating dummy directory: {source_locale_path}.",
|
||||
)
|
||||
|
||||
from ui_translate.settings import ( # pyright: ignore[reportMissingImports]
|
||||
from ui_translate.settings import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||
settings as ui_translate_settings,
|
||||
)
|
||||
from ui_translate.update_ui import ( # pyright: ignore[reportMissingImports]
|
||||
from ui_translate.update_ui import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||
UI_OT_i18n_updatetranslation_init_settings,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ import bpy
|
||||
|
||||
# sys.path.append('C:\Program Files\Python37\Lib\site-packages')
|
||||
import lxml.etree
|
||||
from bspy import Gbxml # pyright: ignore[reportMissingImports]
|
||||
from bspy import ( # ty: ignore[unresolved-import]
|
||||
Gbxml, # pyright: ignore[reportMissingImports]
|
||||
)
|
||||
|
||||
|
||||
class GbxmlExporter:
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
from math import pi
|
||||
from pathlib import Path
|
||||
|
||||
import boltspy as bolts # pyright: ignore[reportMissingImports]
|
||||
import boltspy as bolts # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.material
|
||||
import ifcopenshell.api.project
|
||||
|
||||
@@ -31,7 +31,7 @@ import ifcopenshell.api.spatial
|
||||
import ifcopenshell.api.unit
|
||||
import ifcopenshell.guid
|
||||
import numpy as np
|
||||
import pymeshlab # pyright: ignore[reportMissingImports]
|
||||
import pymeshlab # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||
|
||||
|
||||
class Obj2Ifc:
|
||||
|
||||
@@ -31,7 +31,7 @@ import ifcopenshell.api.spatial
|
||||
import ifcopenshell.api.unit
|
||||
import ifcopenshell.guid
|
||||
import numpy as np
|
||||
import pywavefront # pyright: ignore[reportMissingImports]
|
||||
import pywavefront # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||
|
||||
|
||||
class Obj2Ifc:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user