mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 10:23:41 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
@@ -7,6 +7,9 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
lint-formatting:
|
lint-formatting:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
MIN_IOS_PY_VERSION: "3.10"
|
||||||
|
MIN_BLENDER_PY_VERSION: "3.11"
|
||||||
steps:
|
steps:
|
||||||
- name: Action - checkout repository
|
- name: Action - checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -14,12 +17,12 @@ jobs:
|
|||||||
- name: Action - install python
|
- name: Action - install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: ${{ env.MIN_IOS_PY_VERSION }}
|
||||||
|
|
||||||
- name: Action - install python
|
- name: Action - install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -35,8 +38,8 @@ jobs:
|
|||||||
ERROR=0
|
ERROR=0
|
||||||
# Using 2 Python versions - one minimum required for IfcOpenShell
|
# Using 2 Python versions - one minimum required for IfcOpenShell
|
||||||
# and other that's used by Blender currently.
|
# and other that's used by Blender currently.
|
||||||
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
python${{ env.MIN_IOS_PY_VERSION }} -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
||||||
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
|
python${{ env.MIN_BLENDER_PY_VERSION }} -W error -m compileall -q src/bonsai || ERROR=1
|
||||||
exit $ERROR
|
exit $ERROR
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pyver: [py39, py310, py311, py312, py313, py314]
|
pyver: [py310, py311, py312, py313, py314]
|
||||||
config:
|
config:
|
||||||
- {
|
- {
|
||||||
name: "Windows 64bit",
|
name: "Windows 64bit",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pyver: [py39, py310, py311, py312, py313, py314]
|
pyver: [py310, py311, py312, py313, py314]
|
||||||
config:
|
config:
|
||||||
- {
|
- {
|
||||||
name: "Windows 64bit",
|
name: "Windows 64bit",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ jobs:
|
|||||||
cd ../ifcpatch && make test || ERROR=1
|
cd ../ifcpatch && make test || ERROR=1
|
||||||
pip install -e ../ifctester --no-deps
|
pip install -e ../ifctester --no-deps
|
||||||
cd ../ifctester && make test || ERROR=1
|
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.
|
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
|
||||||
cd ../ifcopenshell-python
|
cd ../ifcopenshell-python
|
||||||
pip install mathutils
|
pip install mathutils
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
/_installed-vs*-x*/
|
/_installed-vs*-x*/
|
||||||
/build/
|
/build/
|
||||||
/src/examples/build/
|
/src/examples/build/
|
||||||
|
# ifctester docs output
|
||||||
|
/src/ifctester/test/build/
|
||||||
|
|
||||||
# output directories
|
# output directories
|
||||||
/cmake/out/
|
/cmake/out/
|
||||||
@@ -80,6 +82,9 @@ src/ifcopenshell-python/test/build
|
|||||||
# bonsai i18n
|
# bonsai i18n
|
||||||
src/bonsai/bonsai/translations.py
|
src/bonsai/bonsai/translations.py
|
||||||
|
|
||||||
|
# bonsai external dependencies (cloned for just ty checks)
|
||||||
|
src/bonsai/external_dependencies/
|
||||||
|
|
||||||
# bonsai test temp files
|
# bonsai test temp files
|
||||||
src/bonsai/test/files/temp
|
src/bonsai/test/files/temp
|
||||||
src/bonsai/test/files/basic.ifc.cache.blend
|
src/bonsai/test/files/basic.ifc.cache.blend
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import hashlib
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ from github import Github
|
|||||||
|
|
||||||
|
|
||||||
def get_repo_tag_names() -> list[str]:
|
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]
|
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")
|
print(f"{len(tag_names)} tag_names found in repo")
|
||||||
return tag_names
|
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}'.")
|
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()
|
start = datetime.datetime.now()
|
||||||
|
|
||||||
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
|
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
|
||||||
@@ -97,7 +102,7 @@ should_release = False
|
|||||||
target_release_tag = ""
|
target_release_tag = ""
|
||||||
TARGET_OS = "windows-x64"
|
TARGET_OS = "windows-x64"
|
||||||
|
|
||||||
git_status = os.popen("git status").read()
|
git_status = subprocess.check_output("git status", text=True)
|
||||||
print(git_status)
|
print(git_status)
|
||||||
|
|
||||||
for tag_name in get_repo_tag_names():
|
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
|
# url_blenderbim_py3x_win_zip
|
||||||
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
|
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_py310_win_zip
|
||||||
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
|
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")
|
print("\n_____ build choco.exe with mono")
|
||||||
|
|
||||||
choco_version = "1.1.0"
|
choco_version = "1.1.0"
|
||||||
os.popen(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet").read()
|
run(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet")
|
||||||
os.popen(f"tar -xzf {choco_version}.tar.gz").read()
|
run(f"tar -xzf {choco_version}.tar.gz")
|
||||||
print("choco tar unpack successful")
|
print("choco tar unpack successful")
|
||||||
os.chdir("choco-1.1.0")
|
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)
|
os.chdir(BLENDERBIM_DIR)
|
||||||
|
|
||||||
if pathlib.Path("/opt/chocolatey/choco.exe").exists():
|
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")
|
print("\n_____ build choco pack")
|
||||||
|
|
||||||
os.popen("mono /opt/chocolatey/choco.exe pack --allow-unofficial").read()
|
run("mono /opt/chocolatey/choco.exe pack --allow-unofficial")
|
||||||
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 setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial'
|
||||||
|
)
|
||||||
|
|
||||||
print("\n_____ build choco push")
|
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"choco push of version: {target_release_tag} successful!")
|
||||||
print(f"it took: {datetime.datetime.now() - start}")
|
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():
|
if not dependency_path.is_dir():
|
||||||
continue
|
continue
|
||||||
dependency_name = dependency_path.name
|
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"
|
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
|
||||||
if tar_path.exists():
|
if tar_path.exists():
|
||||||
print(f"Skipping existing cache: '{tar_path}'")
|
print(f"Skipping existing cache: '{tar_path}'")
|
||||||
|
|||||||
+174
@@ -78,6 +78,139 @@ ignore = [
|
|||||||
"UP032", # Replace .format with f-string
|
"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]
|
[tool.poe.tasks]
|
||||||
|
|
||||||
ruff-main = "ruff check --extend-exclude nix/build-all.py"
|
ruff-main = "ruff check --extend-exclude nix/build-all.py"
|
||||||
@@ -87,6 +220,47 @@ ruff.sequence = ["ruff-main", "ruff-old"]
|
|||||||
|
|
||||||
black = "black ."
|
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"]
|
format.sequence = ["black", "ruff-main", "ruff-old"]
|
||||||
|
|
||||||
cmake-format = "gersemi . --in-place"
|
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):
|
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
||||||
def do_GET(self) -> None:
|
def do_GET(self) -> None:
|
||||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||||
self.server.auth_code = query.get("code", [""])[0] # type: ignore
|
self.server.auth_code = query.get("code", [""])[0]
|
||||||
self.server.auth_state = query.get("state", [""])[0] # type: ignore
|
self.server.auth_state = query.get("state", [""])[0]
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/plain")
|
self.send_header("Content-type", "text/plain")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -255,7 +255,7 @@ class BcfClient:
|
|||||||
project_id: str = "",
|
project_id: str = "",
|
||||||
topics: str = "",
|
topics: str = "",
|
||||||
query_string: Optional[str] = None,
|
query_string: Optional[str] = None,
|
||||||
) -> list[Any]:
|
) -> None:
|
||||||
# return self.get(
|
# return self.get(
|
||||||
# f"/projects/{project_id}/topics",
|
# f"/projects/{project_id}/topics",
|
||||||
# {
|
# {
|
||||||
|
|||||||
@@ -173,16 +173,17 @@ def assert_viewpoints(viewpoints):
|
|||||||
assert viewpoint.snapshot is not None
|
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:
|
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
expected_vp = mdl.VisualizationInfo(
|
expected_vp = mdl.VisualizationInfo(
|
||||||
components=mdl.Components(
|
components=mdl.Components(
|
||||||
view_setup_hints=mdl.ViewSetupHints(
|
|
||||||
spaces_visible=False,
|
|
||||||
space_boundaries_visible=False,
|
|
||||||
openings_visible=False,
|
|
||||||
),
|
|
||||||
selection=expected_selection,
|
selection=expected_selection,
|
||||||
visibility=mdl.ComponentVisibility(
|
visibility=mdl.ComponentVisibility(
|
||||||
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
|
spaces_visible=False,
|
||||||
|
space_boundaries_visible=False,
|
||||||
|
openings_visible=False,
|
||||||
|
),
|
||||||
exceptions=expected_exception,
|
exceptions=expected_exception,
|
||||||
default_visibility=False,
|
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_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),
|
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
||||||
field_of_view=60,
|
field_of_view=60,
|
||||||
|
aspect_ratio=1.0,
|
||||||
),
|
),
|
||||||
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
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
|
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:
|
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
expected_vp = mdl.VisualizationInfo(
|
expected_vp = mdl.VisualizationInfo(
|
||||||
components=mdl.Components(
|
components=mdl.Components(
|
||||||
view_setup_hints=mdl.ViewSetupHints(
|
|
||||||
spaces_visible=False,
|
|
||||||
space_boundaries_visible=False,
|
|
||||||
openings_visible=True,
|
|
||||||
),
|
|
||||||
selection=expected_selection,
|
selection=expected_selection,
|
||||||
visibility=mdl.ComponentVisibility(
|
visibility=mdl.ComponentVisibility(
|
||||||
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
|
spaces_visible=False,
|
||||||
|
space_boundaries_visible=False,
|
||||||
|
openings_visible=True,
|
||||||
|
),
|
||||||
exceptions=expected_exception,
|
exceptions=expected_exception,
|
||||||
default_visibility=True,
|
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_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),
|
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
||||||
field_of_view=60,
|
field_of_view=60,
|
||||||
|
aspect_ratio=1.0,
|
||||||
),
|
),
|
||||||
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
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
|
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
|
||||||
|
|
||||||
# Required for hipped roof generation
|
# 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/prochitecture/bpypolyskel" --no-deps -w wheels/
|
||||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/bpypolyskel.git@pyproject_toml" --no-deps -w wheels/
|
|
||||||
|
|
||||||
# folder for executable files
|
# folder for executable files
|
||||||
mkdir -p build/bonsai/libs/bin
|
mkdir -p build/bonsai/libs/bin
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ class IfcExporter:
|
|||||||
|
|
||||||
def set_header(self):
|
def set_header(self):
|
||||||
self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
|
self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
|
||||||
self.file.header.file_name.time_stamp = (
|
self.file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
|
||||||
datetime.datetime.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
|
|
||||||
)
|
|
||||||
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
||||||
self.file.header.file_name.originating_system = "{} {}".format(
|
self.file.header.file_name.originating_system = "{} {}".format(
|
||||||
self.get_application_name(), tool.Blender.get_bonsai_version()
|
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__))
|
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||||
global_subscription_owner = object()
|
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:
|
def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None:
|
||||||
try:
|
try:
|
||||||
obj.name
|
obj.name
|
||||||
except:
|
except:
|
||||||
# The object is invalid but somehow still has a callback. Clear all
|
# The object is invalid but somehow still has a callback.
|
||||||
# msgbus subscriptions to prevent useless further triggers.
|
# This can occur during undo/redo when the Python wrapper is stale.
|
||||||
bpy.msgbus.clear_by_owner(obj)
|
return
|
||||||
return # In case the object RNA is gone during an undo / redo operation
|
|
||||||
# Blender names are up to 63 UTF-8 bytes
|
# Blender names are up to 63 UTF-8 bytes
|
||||||
if len(bytes(obj.name, "utf-8")) >= 63:
|
if len(bytes(obj.name, "utf-8")) >= 63:
|
||||||
return
|
return
|
||||||
@@ -189,7 +192,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
|
|||||||
return
|
return
|
||||||
bpy.msgbus.subscribe_rna(
|
bpy.msgbus.subscribe_rna(
|
||||||
key=subscribe_to,
|
key=subscribe_to,
|
||||||
owner=obj,
|
owner=object_subscription_owner,
|
||||||
args=(
|
args=(
|
||||||
obj,
|
obj,
|
||||||
data_path,
|
data_path,
|
||||||
|
|||||||
@@ -316,11 +316,8 @@ class IfcStore:
|
|||||||
del IfcStore.id_map[data["id"]]
|
del IfcStore.id_map[data["id"]]
|
||||||
if "guid" in data:
|
if "guid" in data:
|
||||||
del IfcStore.guid_map[data["guid"]]
|
del IfcStore.guid_map[data["guid"]]
|
||||||
obj = IfcStore.get_object_by_name(data["obj"])
|
# Note: msgbus subscriptions are cleared globally during
|
||||||
if obj is None:
|
# rebuild_element_maps which runs after every undo/redo.
|
||||||
# obj was just created during this step and didn't existed before.
|
|
||||||
return
|
|
||||||
bpy.msgbus.clear_by_owner(obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def commit_link_element(data: OperationData) -> None:
|
def commit_link_element(data: OperationData) -> None:
|
||||||
@@ -367,10 +364,8 @@ class IfcStore:
|
|||||||
del IfcStore.id_map[data["id"]]
|
del IfcStore.id_map[data["id"]]
|
||||||
if "guid" in data:
|
if "guid" in data:
|
||||||
del IfcStore.guid_map[data["guid"]]
|
del IfcStore.guid_map[data["guid"]]
|
||||||
obj = IfcStore.get_object_by_name(data["obj"])
|
# Note: msgbus subscriptions are cleared globally during
|
||||||
# obj might be removed after unlink.
|
# rebuild_element_maps which runs after every undo/redo.
|
||||||
if not obj:
|
|
||||||
bpy.msgbus.clear_by_owner(obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unlink_element(
|
def unlink_element(
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ class MaterialCreator:
|
|||||||
mesh: Union[OBJECT_DATA_TYPE, None],
|
mesh: Union[OBJECT_DATA_TYPE, None],
|
||||||
shape_has_openings: bool,
|
shape_has_openings: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
if ((rep := getattr(element, "Representation", ...) is not ...) and not rep) or (
|
if (((rep := getattr(element, "Representation", ...)) is not ... and not rep) or
|
||||||
(rep := getattr(element, "RepresentationMaps", ...) is not ...) and not rep
|
((rep := getattr(element, "RepresentationMaps", ...)) is not ... and not rep)
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class AggregateDecorator:
|
|||||||
cls.is_installed = False
|
cls.is_installed = False
|
||||||
|
|
||||||
def dotted_line_shader(self):
|
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")
|
vert_out.smooth("FLOAT", "v_ArcLength")
|
||||||
|
|
||||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
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]]:
|
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
|
props = self
|
||||||
active_topic = props.active_topic
|
active_topic = props.active_topic
|
||||||
active_related_topics = active_topic.related_topics.keys()
|
active_related_topics = active_topic.related_topics.keys()
|
||||||
|
|||||||
@@ -46,26 +46,26 @@ def get_libraries(self, context):
|
|||||||
|
|
||||||
|
|
||||||
def get_namespaces(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]
|
NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
|
||||||
return NAMESPACES_ENUM_ITEMS
|
return NAMESPACES_ENUM_ITEMS
|
||||||
|
|
||||||
|
|
||||||
def get_brick_entity_classes(self, context):
|
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 = self.brick_entity_create_type
|
||||||
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
|
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
|
||||||
return ENTITY_CLASSES_ENUM_ITEMS
|
return ENTITY_CLASSES_ENUM_ITEMS
|
||||||
|
|
||||||
|
|
||||||
def get_brick_roots(self, context):
|
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]
|
BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes]
|
||||||
return BRICK_ROOTS_ENUM_ITEMS
|
return BRICK_ROOTS_ENUM_ITEMS
|
||||||
|
|
||||||
|
|
||||||
def get_brick_relations(self, context):
|
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]
|
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
|
||||||
for relation in BrickschemaData.data["active_relations"]:
|
for relation in BrickschemaData.data["active_relations"]:
|
||||||
if relation["predicate_name"] == "label":
|
if relation["predicate_name"] == "label":
|
||||||
|
|||||||
@@ -1285,7 +1285,7 @@ class SnapManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
coords = np.empty(vertex_count * 3, dtype=np.float32)
|
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)
|
coords = coords.reshape(-1, 3)
|
||||||
|
|
||||||
matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
|
matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
|
||||||
|
|||||||
@@ -456,7 +456,8 @@ def format_distance(
|
|||||||
tx_dist = fmt % d_cm
|
tx_dist = fmt % d_cm
|
||||||
|
|
||||||
else:
|
else:
|
||||||
tx_dist = fmt % value
|
assert f"Unexpected unit_system - '{unit_system}'."
|
||||||
|
# tx_dist = fmt % value
|
||||||
|
|
||||||
return tx_dist
|
return tx_dist
|
||||||
|
|
||||||
|
|||||||
@@ -1426,6 +1426,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
"/Pset_.*Common/.Status",
|
"/Pset_.*Common/.Status",
|
||||||
"EPset_Status.Status",
|
"EPset_Status.Status",
|
||||||
"EPset_Status.UserDefinedStatus",
|
"EPset_Status.UserDefinedStatus",
|
||||||
|
"Material.Name",
|
||||||
]
|
]
|
||||||
|
|
||||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import ifcopenshell.api.pset
|
|||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
BoolVectorProperty,
|
|
||||||
CollectionProperty,
|
CollectionProperty,
|
||||||
EnumProperty,
|
EnumProperty,
|
||||||
FloatProperty,
|
FloatProperty,
|
||||||
|
|||||||
@@ -366,9 +366,6 @@ class BaseLinesShader(BaseShader):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, gap_size=16):
|
|
||||||
super().__init__(gap_size=gap_size)
|
|
||||||
|
|
||||||
def glenable(self):
|
def glenable(self):
|
||||||
super().glenable()
|
super().glenable()
|
||||||
|
|
||||||
|
|||||||
@@ -816,7 +816,6 @@ class BIM_PT_text(Panel):
|
|||||||
|
|
||||||
for i, literal_data in enumerate(text_data["Literals"]):
|
for i, literal_data in enumerate(text_data["Literals"]):
|
||||||
box = self.layout.box()
|
box = self.layout.box()
|
||||||
box.label(text=f"Literal[{i}]:")
|
|
||||||
|
|
||||||
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
||||||
for attribute in literal_data:
|
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.")
|
cls.poll_message_set("Only available from Outliner.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context): # ty:ignore[override-of-final-method]
|
||||||
if len(getattr(context, "selected_ids", [])) == 0:
|
if len(getattr(context, "selected_ids", [])) == 0:
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -2289,7 +2289,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
elif obj in pprops.clipping_planes_objs:
|
elif obj in pprops.clipping_planes_objs:
|
||||||
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
||||||
elif element:
|
elif element:
|
||||||
if not obj.data:
|
if not obj.data or obj.type not in ("MESH", "CURVE"):
|
||||||
self.report({"INFO"}, "No geometry to edit")
|
self.report({"INFO"}, "No geometry to edit")
|
||||||
elif tool.Geometry.is_locked(element):
|
elif tool.Geometry.is_locked(element):
|
||||||
self.report({"ERROR"}, lock_error_message(obj.name))
|
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(
|
tool.Georeference.set_coordinates(
|
||||||
"blender",
|
"blender",
|
||||||
ifcopenshell.util.geolocation.enh2xyz(
|
ifcopenshell.util.geolocation.enh2xyz(
|
||||||
*local_coordinates,
|
local_coordinates[0],
|
||||||
|
local_coordinates[1],
|
||||||
|
local_coordinates[2],
|
||||||
float(props.blender_offset_x),
|
float(props.blender_offset_x),
|
||||||
float(props.blender_offset_y),
|
float(props.blender_offset_y),
|
||||||
float(props.blender_offset_z),
|
float(props.blender_offset_z),
|
||||||
@@ -162,7 +164,9 @@ def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types
|
|||||||
tool.Georeference.set_coordinates(
|
tool.Georeference.set_coordinates(
|
||||||
"blender",
|
"blender",
|
||||||
ifcopenshell.util.geolocation.enh2xyz(
|
ifcopenshell.util.geolocation.enh2xyz(
|
||||||
*local_coordinates,
|
local_coordinates[0],
|
||||||
|
local_coordinates[1],
|
||||||
|
local_coordinates[2],
|
||||||
float(props.blender_offset_x),
|
float(props.blender_offset_x),
|
||||||
float(props.blender_offset_y),
|
float(props.blender_offset_y),
|
||||||
float(props.blender_offset_z),
|
float(props.blender_offset_z),
|
||||||
@@ -267,6 +271,8 @@ class BIMGeoreferenceProperties(PropertyGroup):
|
|||||||
x_axis_ordinate: str
|
x_axis_ordinate: str
|
||||||
x_axis_is_null: bool
|
x_axis_is_null: bool
|
||||||
|
|
||||||
|
model_is_georeferenced: bool
|
||||||
|
model_crs: str
|
||||||
model_origin: str
|
model_origin: str
|
||||||
model_origin_si: str
|
model_origin_si: str
|
||||||
model_project_north: str
|
model_project_north: str
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from bonsai.bim.prop import StrProperty
|
|||||||
|
|
||||||
class BIMCityJsonProperties(PropertyGroup):
|
class BIMCityJsonProperties(PropertyGroup):
|
||||||
def get_lods(self, context):
|
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]
|
LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
|
||||||
return LODS_ENUM_ITEMS
|
return LODS_ENUM_ITEMS
|
||||||
|
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ class RadianceExporterProperties(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
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:
|
if self.category in spectraldb:
|
||||||
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
|
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -717,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_material = str(material_set_item.Material.id())
|
||||||
|
|
||||||
self.props.material_set_item_attributes.clear()
|
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.is_a("IfcMaterialProfile"):
|
||||||
if material_set_item.Profile and material_set_item.Profile.ProfileName:
|
if material_set_item.Profile and material_set_item.Profile.ProfileName:
|
||||||
@@ -725,6 +729,29 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
|||||||
|
|
||||||
return {"FINISHED"}
|
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):
|
class DisableEditingMaterialSetItem(bpy.types.Operator):
|
||||||
bl_idname = "bim.disable_editing_material_set_item"
|
bl_idname = "bim.disable_editing_material_set_item"
|
||||||
|
|||||||
@@ -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_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
|
||||||
is_parallel23 = tool.Cad.is_x(angle23, (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"
|
fitting_type = "WYE"
|
||||||
|
|
||||||
if not fitting_type:
|
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)
|
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)
|
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||||
radius: bpy.props.FloatProperty(
|
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):
|
def _execute(self, context):
|
||||||
|
|||||||
@@ -151,29 +151,11 @@ class FilledOpeningGenerator:
|
|||||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||||
)
|
)
|
||||||
assert representation
|
assert representation
|
||||||
|
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||||
# Check if mapped representation - PRESERVE the mapping structure
|
else:
|
||||||
if (
|
representation = self.generate_opening_from_filling(
|
||||||
representation.RepresentationType == "MappedRepresentation"
|
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
||||||
and len(representation.Items) == 1
|
)
|
||||||
and representation.Items[0].is_a("IfcMappedItem")
|
|
||||||
):
|
|
||||||
# Store the existing RepresentationMap to reuse it
|
|
||||||
existing_mapping_source = representation.Items[0].MappingSource
|
|
||||||
reuse_mapped_representation = True
|
|
||||||
else:
|
|
||||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
|
||||||
|
|
||||||
if not reuse_mapped_representation:
|
|
||||||
# Check for library template before generating from filling
|
|
||||||
template_rep = self.get_opening_template_from_type(filling)
|
|
||||||
|
|
||||||
if template_rep:
|
|
||||||
representation = template_rep
|
|
||||||
else:
|
|
||||||
representation = self.generate_opening_from_filling(
|
|
||||||
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create mapped representation
|
# Create mapped representation
|
||||||
if reuse_mapped_representation:
|
if reuse_mapped_representation:
|
||||||
@@ -247,109 +229,38 @@ class FilledOpeningGenerator:
|
|||||||
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
||||||
|
|
||||||
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
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.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
|
||||||
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), 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)
|
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:
|
if existing_opening_occurrence:
|
||||||
representation = ifcopenshell.util.representation.get_representation(
|
representation = ifcopenshell.util.representation.get_representation(
|
||||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||||
)
|
)
|
||||||
|
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||||
if (
|
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||||
representation
|
tool.Ifc.get(), representation=representation
|
||||||
and representation.RepresentationType == "MappedRepresentation"
|
)
|
||||||
and len(representation.Items) == 1
|
ifcopenshell.api.geometry.assign_representation(
|
||||||
and representation.Items[0].is_a("IfcMappedItem")
|
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||||
):
|
)
|
||||||
# PRESERVE the mapped structure - reuse the same RepresentationMap
|
else:
|
||||||
existing_mapping_source = representation.Items[0].MappingSource
|
|
||||||
reuse_mapped_representation = True
|
|
||||||
else:
|
|
||||||
representation_to_use = ifcopenshell.util.representation.resolve_representation(representation)
|
|
||||||
|
|
||||||
if not representation_to_use and not reuse_mapped_representation:
|
|
||||||
template_rep = self.get_opening_template_from_type(filling)
|
|
||||||
if template_rep and template_rep.RepresentationType == "Tessellation":
|
|
||||||
representation_to_use = template_rep
|
|
||||||
|
|
||||||
if not representation_to_use and not reuse_mapped_representation and preserved_representation:
|
|
||||||
representation_to_use = preserved_representation
|
|
||||||
|
|
||||||
if not representation_to_use and not reuse_mapped_representation:
|
|
||||||
opening_obj = tool.Ifc.get_object(opening)
|
opening_obj = tool.Ifc.get_object(opening)
|
||||||
if opening_obj:
|
if opening_obj:
|
||||||
tool.Ifc.unlink(element=opening)
|
tool.Ifc.unlink(element=opening)
|
||||||
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
|
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
|
||||||
|
|
||||||
filling_obj = tool.Ifc.get_object(filling)
|
filling_obj = tool.Ifc.get_object(filling)
|
||||||
representation_to_use = self.generate_opening_from_filling(filling, filling_obj)
|
representation = 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:
|
|
||||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
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(
|
# update voided object representation or all it's parts if it's an aggregate
|
||||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
|
||||||
)
|
|
||||||
|
|
||||||
# update voided object representation...
|
|
||||||
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
|
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
|
||||||
for voided_element in voided_elements:
|
for voided_element in voided_elements:
|
||||||
voided_obj = tool.Ifc.get_object(voided_element)
|
voided_obj = tool.Ifc.get_object(voided_element)
|
||||||
@@ -363,36 +274,6 @@ class FilledOpeningGenerator:
|
|||||||
representation=representation,
|
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(
|
def generate_opening_from_filling(
|
||||||
self,
|
self,
|
||||||
filling: ifcopenshell.entity_instance,
|
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)
|
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
|
||||||
|
|
||||||
rep_obj = tool.Geometry.get_geometry_props().representation_obj
|
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)
|
rep_element = tool.Ifc.get_entity(rep_obj)
|
||||||
tool.Model.mark_manual_booleans(rep_element, booleans)
|
tool.Model.mark_manual_booleans(rep_element, booleans)
|
||||||
tool.Geometry.reload_representation(rep_obj)
|
tool.Geometry.reload_representation(rep_obj)
|
||||||
|
|||||||
@@ -694,10 +694,14 @@ def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[
|
|||||||
|
|
||||||
new_settings = settings.copy()
|
new_settings = settings.copy()
|
||||||
new_settings["context"] = box_context
|
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(
|
ifcopenshell.api.geometry.assign_representation(
|
||||||
ifc_file,
|
ifc_file,
|
||||||
should_run_listeners=False,
|
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||||
product=product,
|
product=product,
|
||||||
representation=new_box,
|
representation=new_box,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
from math import atan2, degrees, pi, radians
|
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 bpy
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
@@ -49,7 +49,7 @@ ProfileFrom2PointsReturn = Union[dict[str, Any], None]
|
|||||||
|
|
||||||
|
|
||||||
class DumbProfileGenerator:
|
class DumbProfileGenerator:
|
||||||
def __init__(self, relating_type):
|
def __init__(self, relating_type: ifcopenshell.entity_instance):
|
||||||
self.relating_type = relating_type
|
self.relating_type = relating_type
|
||||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ class DumbProfileGenerator:
|
|||||||
|
|
||||||
|
|
||||||
class DumbProfileRegenerator:
|
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()
|
self.file = tool.Ifc.get()
|
||||||
objs = []
|
objs = []
|
||||||
if not profile:
|
if not profile:
|
||||||
@@ -221,7 +221,7 @@ class DumbProfileRegenerator:
|
|||||||
for element in self.get_element_types_using_profile(profile):
|
for element in self.get_element_types_using_profile(profile):
|
||||||
tool.Model.mark_thumbnail_for_update(element)
|
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
|
self.file = ifc_file
|
||||||
objs = []
|
objs = []
|
||||||
profile = settings["profile"].Profile
|
profile = settings["profile"].Profile
|
||||||
@@ -233,7 +233,7 @@ class DumbProfileRegenerator:
|
|||||||
objs.append(obj)
|
objs.append(obj)
|
||||||
DumbProfileRecalculator().recalculate(objs)
|
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 = []
|
results = []
|
||||||
profile_sets = [
|
profile_sets = [
|
||||||
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
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)
|
results.extend(rel.RelatedObjects)
|
||||||
return results
|
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 = []
|
results = []
|
||||||
profile_sets = [
|
profile_sets = [
|
||||||
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
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_idname = "bim.extend_profile"
|
||||||
bl_label = "Extend Profile"
|
bl_label = "Extend Profile"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
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):
|
def _execute(self, context):
|
||||||
selected_objs = context.selected_objects
|
selected_objs = context.selected_objects
|
||||||
joiner = DumbProfileJoiner()
|
joiner = DumbProfileJoiner()
|
||||||
if not self.join_type:
|
if self.join_type == "-":
|
||||||
for obj in selected_objs:
|
for obj in selected_objs:
|
||||||
joiner.unjoin(obj)
|
joiner.unjoin(obj)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -626,11 +634,15 @@ class DumbProfileJoiner:
|
|||||||
if connection1 == "ATEND":
|
if connection1 == "ATEND":
|
||||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
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)
|
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
|
self.body[1] = intersect
|
||||||
else:
|
else:
|
||||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
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)
|
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||||
|
|
||||||
@@ -673,11 +685,15 @@ class DumbProfileJoiner:
|
|||||||
elif connection1 == "ATSTART":
|
elif connection1 == "ATSTART":
|
||||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
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)
|
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
|
self.body[0] = intersect
|
||||||
else:
|
else:
|
||||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
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)
|
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||||
|
|
||||||
@@ -721,7 +737,9 @@ class DumbProfileJoiner:
|
|||||||
if connection1 == "ATEND":
|
if connection1 == "ATEND":
|
||||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
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)
|
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
|
self.body[1] = intersect
|
||||||
else:
|
else:
|
||||||
plane = self.get_profile_plane(
|
plane = self.get_profile_plane(
|
||||||
@@ -729,7 +747,9 @@ class DumbProfileJoiner:
|
|||||||
furthest_plane if is_relating else closest_plane,
|
furthest_plane if is_relating else closest_plane,
|
||||||
z_inwards=False if is_relating else True,
|
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)
|
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||||
self.clippings.append(
|
self.clippings.append(
|
||||||
@@ -742,7 +762,9 @@ class DumbProfileJoiner:
|
|||||||
elif connection1 == "ATSTART":
|
elif connection1 == "ATSTART":
|
||||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
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)
|
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
|
self.body[0] = intersect
|
||||||
else:
|
else:
|
||||||
plane = self.get_profile_plane(
|
plane = self.get_profile_plane(
|
||||||
@@ -750,7 +772,9 @@ class DumbProfileJoiner:
|
|||||||
furthest_plane if is_relating else closest_plane,
|
furthest_plane if is_relating else closest_plane,
|
||||||
z_inwards=False if is_relating else True,
|
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)
|
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||||
self.clippings.append(
|
self.clippings.append(
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ def calculate_quantities(usecase_path, ifc_file: ifcopenshell.file, settings):
|
|||||||
return
|
return
|
||||||
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
|
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
|
||||||
qto = ifcopenshell.api.pset.add_qto(
|
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(
|
ifcopenshell.api.pset.edit_qto(
|
||||||
ifc_file,
|
ifc_file,
|
||||||
should_run_listeners=False,
|
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||||
qto=qto,
|
qto=qto,
|
||||||
properties={
|
properties={
|
||||||
"StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days,
|
"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)
|
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
|
||||||
return 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):
|
def set_axis(self, wall, p1, p2):
|
||||||
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
||||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||||
@@ -1333,29 +1312,6 @@ class DumbWallJoiner:
|
|||||||
self.set_axis(element1, p1, p2)
|
self.set_axis(element1, p1, p2)
|
||||||
tool.Model.recreate_wall(element1, wall1)
|
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:
|
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
|
||||||
wall1 = tool.Ifc.get_entity(obj1)
|
wall1 = tool.Ifc.get_entity(obj1)
|
||||||
wall2 = tool.Ifc.get_entity(obj2)
|
wall2 = tool.Ifc.get_entity(obj2)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class NestDecorator:
|
|||||||
cls.is_installed = False
|
cls.is_installed = False
|
||||||
|
|
||||||
def dotted_line_shader(self):
|
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")
|
vert_out.smooth("FLOAT", "v_ArcLength")
|
||||||
|
|
||||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ classes = (
|
|||||||
operator.UnlinkIfc,
|
operator.UnlinkIfc,
|
||||||
operator.UnloadLink,
|
operator.UnloadLink,
|
||||||
workspace.ExploreHotkey,
|
workspace.ExploreHotkey,
|
||||||
workspace.GenerateUVMap,
|
operator.GenerateUVMap,
|
||||||
prop.LibraryBreadcrumb,
|
prop.LibraryBreadcrumb,
|
||||||
prop.LibraryElement,
|
prop.LibraryElement,
|
||||||
prop.FilterCategory,
|
prop.FilterCategory,
|
||||||
|
|||||||
@@ -178,9 +178,18 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
bl_description = (
|
bl_description = (
|
||||||
"Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file."
|
"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"})
|
filter_glob: bpy.props.StringProperty(
|
||||||
append_all: bpy.props.BoolProperty(default=False)
|
default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}
|
||||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
) # 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
|
reload_previous_file = False
|
||||||
|
|
||||||
@@ -558,7 +567,11 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
|
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.append_library_element_by_query"
|
bl_idname = "bim.append_library_element_by_query"
|
||||||
bl_label = "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
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -587,9 +600,16 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
"Append element to the current project.\n\n"
|
"Append element to the current project.\n\n"
|
||||||
"ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)"
|
"ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)"
|
||||||
)
|
)
|
||||||
definition: bpy.props.IntProperty()
|
definition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||||
prop_index: bpy.props.IntProperty()
|
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"})
|
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
|
file: ifcopenshell.file
|
||||||
|
|
||||||
@@ -618,8 +638,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if not element:
|
if not element:
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
if element.is_a("IfcTypeProduct"):
|
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)
|
self.import_type_from_ifc(element, context)
|
||||||
elif element.is_a("IfcProduct"):
|
elif element.is_a("IfcProduct"):
|
||||||
# NOTE: Non-types are not exposed in UI directly
|
# 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()):
|
if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
|
||||||
ifc_importer.create_style(element)
|
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):
|
class EditProjectLibrary(bpy.types.Operator):
|
||||||
bl_idname = "bim.edit_project_library"
|
bl_idname = "bim.edit_project_library"
|
||||||
@@ -988,24 +959,28 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
bl_label = "Load Project"
|
bl_label = "Load Project"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Load an existing IFC project"
|
bl_description = "Load an existing IFC project"
|
||||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
filepath: bpy.props.StringProperty(
|
||||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"})
|
subtype="FILE_PATH", options={"SKIP_SAVE"}
|
||||||
is_advanced: bpy.props.BoolProperty(
|
) # 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",
|
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",
|
description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
use_relative_path: bpy.props.BoolProperty(
|
use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||||
name="Use Relative Path",
|
name="Use Relative Path",
|
||||||
description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved",
|
description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
should_start_fresh_session: bpy.props.BoolProperty(
|
should_start_fresh_session: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||||
name="Should Start Fresh Session",
|
name="Should Start Fresh Session",
|
||||||
description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option",
|
description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option",
|
||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
import_without_ifc_data: bpy.props.BoolProperty(
|
import_without_ifc_data: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||||
name="Import Without IFC Data",
|
name="Import Without IFC Data",
|
||||||
description=(
|
description=(
|
||||||
"Import IFC objects as Blender objects without any IFC metadata and authoring capabilities."
|
"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,
|
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"
|
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
|
@classmethod
|
||||||
def description(cls, context, properties):
|
def description(cls, context, properties):
|
||||||
tooltip = cls.bl_description
|
tooltip = cls.bl_description
|
||||||
@@ -1314,7 +1300,10 @@ class ToggleFilterCategories(bpy.types.Operator):
|
|||||||
bl_idname = "bim.toggle_filter_categories"
|
bl_idname = "bim.toggle_filter_categories"
|
||||||
bl_label = "Toggle Filter Categories"
|
bl_label = "Toggle Filter Categories"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
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):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
@@ -1338,6 +1327,14 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
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"
|
filename_ext = ".ifc"
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -1347,20 +1344,25 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
|||||||
filter_glob: str
|
filter_glob: str
|
||||||
use_relative_path: bool
|
use_relative_path: bool
|
||||||
use_cache: bool
|
use_cache: bool
|
||||||
|
query: str
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
assert self.layout
|
||||||
pprops = tool.Project.get_project_props()
|
pprops = tool.Project.get_project_props()
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(self, "use_relative_path")
|
row.prop(self, "use_relative_path")
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(self, "use_cache")
|
row.prop(self, "use_cache")
|
||||||
row = self.layout.row()
|
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":
|
if pprops.false_origin_mode == "MANUAL":
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(pprops, "false_origin")
|
row.prop(pprops, "false_origin")
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(pprops, "project_north")
|
row.prop(pprops, "project_north")
|
||||||
|
self.layout.prop(self, "query", placeholder="IfcElement")
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
start = time.time()
|
start = time.time()
|
||||||
@@ -1393,7 +1395,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
|||||||
new.ifc_definition_id = reference.id()
|
new.ifc_definition_id = reference.id()
|
||||||
new.name = filepath
|
new.name = filepath
|
||||||
new.filepath = 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):
|
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_label = "Unlink IFC"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Remove the selected file from the link list"
|
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):
|
def _execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
@@ -1421,7 +1427,11 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_label = "Unload Link"
|
bl_label = "Unload Link"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Unload the selected linked file"
|
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):
|
def _execute(self, context):
|
||||||
link = tool.Project.get_project_props().links[self.link_index]
|
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]
|
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
||||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # 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:
|
if TYPE_CHECKING:
|
||||||
link_index: int
|
link_index: int
|
||||||
use_cache: bool
|
use_cache: bool
|
||||||
|
query: str
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
self.link = tool.Project.get_project_props().links[self.link_index]
|
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]:
|
def link_ifc(self) -> Union[set[str], None]:
|
||||||
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
|
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
|
||||||
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
|
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)
|
os.remove(blend_filepath)
|
||||||
|
|
||||||
if not blend_filepath.exists():
|
if not blend_filepath.exists():
|
||||||
@@ -1520,7 +1544,7 @@ def run():
|
|||||||
pprops.project_north = "{pprops.project_north}"
|
pprops.project_north = "{pprops.project_north}"
|
||||||
# Use absolute path to be safe from cwd changes.
|
# Use absolute path to be safe from cwd changes.
|
||||||
try:
|
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:
|
except RuntimeError as e:
|
||||||
# Operator failed (returned CANCELLED with error report)
|
# Operator failed (returned CANCELLED with error report)
|
||||||
print(f"Failed to load linked project: {{e}}")
|
print(f"Failed to load linked project: {{e}}")
|
||||||
@@ -1606,7 +1630,11 @@ class ReloadLink(bpy.types.Operator):
|
|||||||
bl_label = "Reload Link"
|
bl_label = "Reload Link"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Reload the selected file"
|
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):
|
def execute(self, context):
|
||||||
bpy.ops.bim.unload_link(link_index=self.link_index)
|
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_label = "Toggle Link Selectability"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Toggle selectability"
|
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):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
@@ -1788,7 +1820,11 @@ class SelectLinkHandle(bpy.types.Operator):
|
|||||||
bl_label = "Select Link Handle"
|
bl_label = "Select Link Handle"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Select link empty object handle"
|
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):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
@@ -1846,11 +1882,28 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
filename_ext = ".ifc"
|
filename_ext = ".ifc"
|
||||||
supported_filexts = (".ifc", ".ifczip", ".ifcjson")
|
supported_filexts = (".ifc", ".ifczip", ".ifcjson")
|
||||||
filter_glob: bpy.props.StringProperty(default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"})
|
filter_glob: bpy.props.StringProperty(
|
||||||
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version")
|
default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}
|
||||||
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"})
|
json_version: bpy.props.EnumProperty(
|
||||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
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
|
@classmethod
|
||||||
def poll(cls, context):
|
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_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||||
|
"""See ``bim.link_ifc``."""
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
query: str
|
||||||
|
|
||||||
file: ifcopenshell.file
|
file: ifcopenshell.file
|
||||||
meshes: dict[str, bpy.types.Mesh]
|
meshes: dict[str, bpy.types.Mesh]
|
||||||
# Material names is derived from diffuse as in 'r-g-b-a'.
|
# 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.context_settings = tool.Loader.create_settings()
|
||||||
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
||||||
|
|
||||||
self.elements = set(self.file.by_type("IfcElement"))
|
if self.query:
|
||||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
self.elements = ifcopenshell.util.selector.filter_elements(self.file, self.query)
|
||||||
self.elements |= set(self.file.by_type("IfcProxy"))
|
|
||||||
if self.file.schema == "IFC2X3":
|
|
||||||
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
|
|
||||||
else:
|
else:
|
||||||
self.elements |= set(self.file.by_type("IfcSpatialElement"))
|
self.elements = set(self.file.by_type("IfcElement"))
|
||||||
self.elements -= set(self.file.by_type("IfcFeatureElement"))
|
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:
|
if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin:
|
||||||
tool.Loader.set_manual_blender_offset(self.file)
|
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_mode": pprops.false_origin_mode,
|
||||||
"false_origin": pprops.false_origin,
|
"false_origin": pprops.false_origin,
|
||||||
"project_north": pprops.project_north,
|
"project_north": pprops.project_north,
|
||||||
|
"query": self.query,
|
||||||
}
|
}
|
||||||
with open(self.json_filepath, "w") as f:
|
with open(self.json_filepath, "w") as f:
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
@@ -2495,7 +2558,7 @@ class EnableCulling(bpy.types.Operator):
|
|||||||
self.total_mousemoves = 0
|
self.total_mousemoves = 0
|
||||||
self.cullable_objects = []
|
self.cullable_objects = []
|
||||||
|
|
||||||
def modal(self, context, event):
|
def modal(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
|
||||||
if not LinksData.enable_culling:
|
if not LinksData.enable_culling:
|
||||||
for obj in bpy.context.visible_objects:
|
for obj in bpy.context.visible_objects:
|
||||||
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
||||||
@@ -2526,7 +2589,7 @@ class EnableCulling(bpy.types.Operator):
|
|||||||
|
|
||||||
return {"PASS_THROUGH"}
|
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
|
view_matrix = context.region_data.view_matrix
|
||||||
projection_matrix = context.region_data.window_matrix
|
projection_matrix = context.region_data.window_matrix
|
||||||
vp_matrix = projection_matrix @ view_matrix
|
vp_matrix = projection_matrix @ view_matrix
|
||||||
@@ -2541,7 +2604,7 @@ class EnableCulling(bpy.types.Operator):
|
|||||||
return True
|
return True
|
||||||
return False
|
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
|
# Get the view matrix and the projection matrix from the active viewport
|
||||||
view_matrix = context.region_data.view_matrix
|
view_matrix = context.region_data.view_matrix
|
||||||
projection_matrix = context.region_data.window_matrix
|
projection_matrix = context.region_data.window_matrix
|
||||||
@@ -2568,7 +2631,7 @@ class EnableCulling(bpy.types.Operator):
|
|||||||
return False
|
return False
|
||||||
return True
|
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
|
LinksData.enable_culling = True
|
||||||
self.cullable_objects = []
|
self.cullable_objects = []
|
||||||
for obj in bpy.context.visible_objects:
|
for obj in bpy.context.visible_objects:
|
||||||
@@ -2855,8 +2918,16 @@ class IFCFileHandlerOperator(bpy.types.Operator):
|
|||||||
bl_label = "Import .ifc file"
|
bl_label = "Import .ifc file"
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
|
directory: bpy.props.StringProperty(
|
||||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
|
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):
|
def invoke(self, context, event):
|
||||||
# Keeping code in .invoke() as we'll probably add some
|
# 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_label = "Measure Tool"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
measure_type: bpy.props.StringProperty()
|
measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
measure_type: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -3003,7 +3077,10 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
|
|||||||
bl_label = "Measure Face Area Tool"
|
bl_label = "Measure Face Area Tool"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
measure_type: bpy.props.StringProperty()
|
measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
measure_type: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -3107,7 +3184,10 @@ class ClearMeasurement(bpy.types.Operator):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
polyline_props = tool.Model.get_polyline_props()
|
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):
|
def execute(self, context):
|
||||||
polyline_props = tool.Model.get_polyline_props()
|
polyline_props = tool.Model.get_polyline_props()
|
||||||
@@ -3211,7 +3291,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
|||||||
super().invoke(context, event)
|
super().invoke(context, event)
|
||||||
return {"RUNNING_MODAL"}
|
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)
|
context.workspace.status_text_set(text=None)
|
||||||
if hasattr(self, "tool_state"):
|
if hasattr(self, "tool_state"):
|
||||||
self.tool_state.plane_method = None
|
self.tool_state.plane_method = None
|
||||||
@@ -3219,7 +3299,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
|||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
def handle_custom_instructions(self, context):
|
def handle_custom_instructions(self, context: bpy.types.Context) -> None:
|
||||||
if len(self.selected_points) == 0:
|
if len(self.selected_points) == 0:
|
||||||
instruction_text = "Click First Point on Image"
|
instruction_text = "Click First Point on Image"
|
||||||
elif len(self.selected_points) == 1:
|
elif len(self.selected_points) == 1:
|
||||||
@@ -3234,14 +3314,14 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
|||||||
|
|
||||||
context.workspace.status_text_set(text=instruction_text)
|
context.workspace.status_text_set(text=instruction_text)
|
||||||
|
|
||||||
def calculate_distance(self):
|
def calculate_distance(self) -> None:
|
||||||
if len(self.selected_points) == 2:
|
if len(self.selected_points) == 2:
|
||||||
point1 = self.selected_points[0]
|
point1 = self.selected_points[0]
|
||||||
point2 = self.selected_points[1]
|
point2 = self.selected_points[1]
|
||||||
distance_3d = (point2 - point1).length
|
distance_3d = (point2 - point1).length
|
||||||
self.calculated_distance = distance_3d / self.unit_scale
|
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:
|
if len(self.selected_points) != 2:
|
||||||
self.report({"ERROR"}, "Two points must be selected")
|
self.report({"ERROR"}, "Two points must be selected")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
@@ -3299,7 +3379,10 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
|||||||
bl_idname = "bim.load_blend_metadata_and_ifc"
|
bl_idname = "bim.load_blend_metadata_and_ifc"
|
||||||
bl_label = "Load Blend Metadata and IFC"
|
bl_label = "Load Blend Metadata and IFC"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
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):
|
def execute(self, context):
|
||||||
ifc_file = self.filepath
|
ifc_file = self.filepath
|
||||||
@@ -3333,3 +3416,19 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
|||||||
bpy.app.handlers.load_post.append(load_handler)
|
bpy.app.handlers.load_post.append(load_handler)
|
||||||
bpy.ops.wm.open_mainfile(filepath=metadata_path)
|
bpy.ops.wm.open_mainfile(filepath=metadata_path)
|
||||||
return {"FINISHED"}
|
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
|
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
|
||||||
else:
|
else:
|
||||||
row.operator("bim.load_link", text="", icon="LINKED").link_index = index
|
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")
|
self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index")
|
||||||
|
|
||||||
if LinksData.enable_culling:
|
if LinksData.enable_culling:
|
||||||
|
|||||||
@@ -71,24 +71,26 @@ class ExploreTool(bpy.types.WorkSpaceTool):
|
|||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text="", icon="EVENT_SHIFT")
|
row.label(text="", icon="EVENT_SHIFT")
|
||||||
row.label(text="", icon="EVENT_M")
|
row.label(text="", icon="EVENT_M")
|
||||||
row = layout.row(align=True)
|
|
||||||
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
|
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
|
||||||
op.hotkey = "S_M"
|
op.hotkey = "S_M"
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=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")
|
op = row.operator("bim.clear_measurement", text="", icon="X")
|
||||||
|
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text="", icon="EVENT_SHIFT")
|
row.label(text="", icon="EVENT_SHIFT")
|
||||||
row.label(text="", icon="EVENT_S")
|
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 = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE")
|
||||||
op.hotkey = "S_S"
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
op = row.operator("bim.generate_uv_map", text="Generate UV Map", icon="UV")
|
row = layout.row(align=True)
|
||||||
op.description = "Generate UV map for selected mesh."
|
row.operator("bim.generate_uv_map", icon="UV")
|
||||||
|
|
||||||
|
|
||||||
class ExploreHotkey(bpy.types.Operator):
|
class ExploreHotkey(bpy.types.Operator):
|
||||||
@@ -156,20 +158,3 @@ class ExploreHotkey(bpy.types.Operator):
|
|||||||
|
|
||||||
def hotkey_A_H(self) -> None:
|
def hotkey_A_H(self) -> None:
|
||||||
bpy.ops.bim.hide_queried_linked_element(unhide_all=True)
|
bpy.ops.bim.hide_queried_linked_element(unhide_all=True)
|
||||||
|
|
||||||
|
|
||||||
class GenerateUVMap(bpy.types.Operator):
|
|
||||||
bl_idname = "bim.generate_uv_map"
|
|
||||||
bl_label = "Generate UV Map"
|
|
||||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
||||||
|
|
||||||
description: bpy.props.StringProperty()
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
obj = context.active_object
|
|
||||||
if not obj or not hasattr(obj, "data") or not hasattr(obj.data, "polygons"):
|
|
||||||
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"}
|
|
||||||
@@ -321,7 +321,7 @@ def get_gross_perimeter(o: bpy.types.Object) -> float:
|
|||||||
return gross_perimeter
|
return gross_perimeter
|
||||||
|
|
||||||
|
|
||||||
def get_space_net_perimeter(obj: bpy.types.Object) -> float:
|
def get_space_net_perimeter(obj: bpy.types.Object) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ class SelectFilterElements(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class ApplyFilterFromText(Operator, tool.Ifc.Operator):
|
class ApplyFilterFromText(Operator):
|
||||||
bl_idname = "bim.apply_filter_from_text"
|
bl_idname = "bim.apply_filter_from_text"
|
||||||
bl_label = "Apply Filter Configuration"
|
bl_label = "Apply Filter Configuration"
|
||||||
bl_description = "Apply the JSON filter configuration from the current text block"
|
bl_description = "Apply the JSON filter configuration from the current text block"
|
||||||
@@ -1440,7 +1440,7 @@ class ShowAllElements(Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class SelectSimilar(Operator, tool.Ifc.Operator):
|
class SelectSimilar(Operator):
|
||||||
bl_idname = "bim.select_similar"
|
bl_idname = "bim.select_similar"
|
||||||
bl_label = "Select Similar"
|
bl_label = "Select Similar"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import ifcopenshell.util.representation
|
|||||||
import ifcopenshell.util.unit
|
import ifcopenshell.util.unit
|
||||||
import ifcopenshell.util.unit as ifcunit
|
import ifcopenshell.util.unit as ifcunit
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
@@ -478,7 +479,7 @@ class ShaderInfo:
|
|||||||
"""get the args to the point shader"""
|
"""get the args to the point shader"""
|
||||||
location = np.array(location)
|
location = np.array(location)
|
||||||
indices = []
|
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))),
|
"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))),
|
"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))),
|
"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,
|
PARALLEL DISTRIBUTED FORCE,
|
||||||
DISTRIBUTED MOMENT,
|
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", "forces")
|
||||||
vert_out.smooth("VEC3", "co")
|
vert_out.smooth("VEC3", "co")
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ class DecorationShader:
|
|||||||
"""param: pattern: type of pattern
|
"""param: pattern: type of pattern
|
||||||
SINGLE FORCE,
|
SINGLE FORCE,
|
||||||
SINGLE MOMENT"""
|
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")
|
vert_out.smooth("VEC3", "co")
|
||||||
|
|
||||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||||
@@ -253,7 +253,7 @@ class DecorationShader:
|
|||||||
|
|
||||||
def get_planar_shader(self) -> gpu.types.GPUShader:
|
def get_planar_shader(self) -> gpu.types.GPUShader:
|
||||||
"""shader for planar loads"""
|
"""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")
|
vert_out.smooth("VEC3", "co")
|
||||||
|
|
||||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||||
|
|||||||
@@ -787,7 +787,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
|||||||
doc: DocPreferences
|
doc: DocPreferences
|
||||||
default_parameters: DefaultParameters
|
default_parameters: DefaultParameters
|
||||||
container_hide_show_isolate: bool
|
container_hide_show_isolate: bool
|
||||||
mass_time_units_in_wizard: bool
|
|
||||||
chain_filter_with_set_operations: bool
|
chain_filter_with_set_operations: bool
|
||||||
save_metadata_blend_file: bool
|
save_metadata_blend_file: bool
|
||||||
metadata_blend_file_suffix: str
|
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:
|
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||||
layout.prop(self, "container_hide_show_isolate")
|
layout.prop(self, "container_hide_show_isolate")
|
||||||
layout.prop(self, "mass_time_units_in_wizard")
|
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.prop(self, "chain_filter_with_set_operations")
|
row.prop(self, "chain_filter_with_set_operations")
|
||||||
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
|
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
|
||||||
|
|||||||
@@ -432,10 +432,7 @@ def update_drawing_name(
|
|||||||
if drawing_tool.get_name(drawing) != name:
|
if drawing_tool.get_name(drawing) != name:
|
||||||
ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name})
|
ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name})
|
||||||
|
|
||||||
# Update the camera object name
|
drawing_tool.set_camera_name(drawing, name)
|
||||||
camera = ifc.get_object(drawing)
|
|
||||||
if camera and camera.name != name:
|
|
||||||
camera.name = name
|
|
||||||
|
|
||||||
group = drawing_tool.get_drawing_group(drawing)
|
group = drawing_tool.get_drawing_group(drawing)
|
||||||
if drawing_tool.get_name(group) != name:
|
if drawing_tool.get_name(group) != name:
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def align_walls(
|
|||||||
align_type: AlignType,
|
align_type: AlignType,
|
||||||
):
|
):
|
||||||
reference_obj = blender.get_active_object(is_selected=True)
|
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
|
reference_obj = None
|
||||||
objs = [
|
objs = [
|
||||||
o
|
o
|
||||||
@@ -159,48 +159,6 @@ def extend_wall_to_slab(
|
|||||||
model.reload_body_representation(wall_objs)
|
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):
|
class RequireTwoWallsError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -349,7 +349,10 @@ class Drawing:
|
|||||||
def enable_editing_text(cls, obj): pass
|
def enable_editing_text(cls, obj): pass
|
||||||
def ensure_unique_drawing_name(cls, name): pass
|
def ensure_unique_drawing_name(cls, name): pass
|
||||||
def ensure_unique_identification(cls, identification): 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_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_matrix(cls, target_view, location_hint): pass
|
||||||
def generate_drawing_name(cls, target_view, location_hint): pass
|
def generate_drawing_name(cls, target_view, location_hint): pass
|
||||||
def generate_reference_attributes(cls, reference, **attributes): 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_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 run_type_assign_type(cls, element=None, relating_type=None): pass
|
||||||
def select_assigned_product(cls, drawing): 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_drawing_collection_name(cls, drawing, collection): pass
|
||||||
def set_name(cls, element, name): pass
|
def set_name(cls, element, name): pass
|
||||||
def setup_annotation_object(cls, obj, object_type): pass
|
def setup_annotation_object(cls, obj, object_type): pass
|
||||||
@@ -620,7 +624,10 @@ class Model:
|
|||||||
def import_rectangle(cls, obj, position, profile): pass
|
def import_rectangle(cls, obj, position, profile): pass
|
||||||
def load_openings(cls, openings): pass
|
def load_openings(cls, openings): pass
|
||||||
def purge_scene_openings(cls): pass
|
def purge_scene_openings(cls): pass
|
||||||
|
def recalculate_walls(cls, objs): pass
|
||||||
def regenerate_array(cls, parent, data): 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 reload_body_representation(cls, obj_or_objects): pass
|
||||||
def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass
|
def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass
|
||||||
|
|
||||||
@@ -1119,6 +1126,8 @@ class Type:
|
|||||||
def get_representation_context(cls, representation): pass
|
def get_representation_context(cls, representation): pass
|
||||||
def get_type_occurrences(cls, element_type): pass
|
def get_type_occurrences(cls, element_type): pass
|
||||||
def has_material_usage(cls, element): 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_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
|
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)
|
related_object = tool.Ifc.get_entity(related_obj)
|
||||||
if not relating_object or not related_object:
|
if not relating_object or not related_object:
|
||||||
return False
|
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(
|
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a(
|
||||||
"IfcElement"
|
"IfcElement"
|
||||||
):
|
):
|
||||||
return True
|
is_compatible_class = True
|
||||||
if tool.Ifc.get_schema() == "IFC2X3":
|
elif tool.Ifc.get_schema() == "IFC2X3":
|
||||||
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
|
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
|
||||||
return True
|
is_compatible_class = True
|
||||||
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
|
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
|
||||||
return True
|
is_compatible_class = True
|
||||||
else:
|
else:
|
||||||
if relating_object.is_a("IfcSpatialElement") and related_object.is_a("IfcSpatialElement"):
|
if relating_object.is_a("IfcSpatialElement") and related_object.is_a("IfcSpatialElement"):
|
||||||
return True
|
is_compatible_class = True
|
||||||
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
|
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
|
||||||
return True
|
is_compatible_class = True
|
||||||
return False
|
|
||||||
|
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
|
@classmethod
|
||||||
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
|
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import bonsai.core.tool
|
|||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from bsdd.bsdd import ClassContractV1, ClassPropertyContractV1, PropertyContractV5
|
||||||
|
|
||||||
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDDictionary
|
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDDictionary
|
||||||
|
|
||||||
|
|
||||||
@@ -39,8 +41,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
|||||||
default_identifier_url = "https://identifier.buildingsmart.org"
|
default_identifier_url = "https://identifier.buildingsmart.org"
|
||||||
default_api_url = "https://api.bsdd.buildingsmart.org/api/"
|
default_api_url = "https://api.bsdd.buildingsmart.org/api/"
|
||||||
client = bsdd.Client()
|
client = bsdd.Client()
|
||||||
bsdd_classes: dict[str, dict] = {}
|
bsdd_classes: dict[str, ClassContractV1] = {}
|
||||||
bsdd_properties: dict[str, dict] = {}
|
bsdd_properties: dict[str, ClassPropertyContractV1 | PropertyContractV5] = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def identifier_url(cls) -> str:
|
def identifier_url(cls) -> str:
|
||||||
@@ -267,7 +269,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_bsdd_property(cls, uri: str) -> dict:
|
def get_bsdd_property(cls, uri: str) -> dict:
|
||||||
if not (bsdd_property := cls.bsdd_properties.get(uri, {})):
|
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
|
cls.bsdd_properties[uri] = bsdd_property
|
||||||
return bsdd_property
|
return bsdd_property
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class Cost(bonsai.core.tool.Cost):
|
|||||||
device = aud.Device()
|
device = aud.Device()
|
||||||
# chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/
|
# 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__()
|
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)
|
device.play(sound)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -857,6 +857,8 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None:
|
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 (element := tool.Ifc.get_entity(obj))
|
||||||
assert (rep := cls.get_annotation_representation(element))
|
assert (rep := cls.get_annotation_representation(element))
|
||||||
to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
|
to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
|
||||||
@@ -1288,6 +1290,12 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
def get_representation(cls, element, context):
|
def get_representation(cls, element, context):
|
||||||
return ifcopenshell.util.representation.get_representation(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
|
@classmethod
|
||||||
def set_drawing_collection_name(
|
def set_drawing_collection_name(
|
||||||
cls, drawing: ifcopenshell.entity_instance, collection: bpy.types.Collection
|
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 representation_item: item to remove.
|
||||||
:param element: item's element. Is used to unmark manual booleans.
|
: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`
|
# NOTE: a lot of shared code with `geometry.remove_representation`
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
shape_aspects: list[ifcopenshell.entity_instance] = []
|
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)
|
cls.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
|
||||||
|
|
||||||
if representation:
|
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)
|
also_consider = list(consider_inverses)
|
||||||
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
|
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():
|
if not cls.get():
|
||||||
return
|
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:
|
for obj in bpy.data.objects:
|
||||||
if obj.library:
|
if obj.library:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
bpy.msgbus.clear_by_owner(obj)
|
|
||||||
|
|
||||||
element = cls.get_entity(obj)
|
element = cls.get_entity(obj)
|
||||||
if not element:
|
if not element:
|
||||||
continue
|
continue
|
||||||
@@ -217,8 +221,6 @@ class Ifc(bonsai.core.tool.Ifc):
|
|||||||
if obj.library:
|
if obj.library:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
bpy.msgbus.clear_by_owner(obj)
|
|
||||||
|
|
||||||
style = cls.get_entity(obj)
|
style = cls.get_entity(obj)
|
||||||
if not style:
|
if not style:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ class IfcGit:
|
|||||||
if re.match("^Ifc", obj.name):
|
if re.match("^Ifc", obj.name):
|
||||||
bpy.data.objects.remove(obj, do_unlink=True)
|
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 = import_ifc.IfcImportSettings.factory(bpy.context, path_ifc, logging.getLogger("ImportIFC"))
|
||||||
settings.should_setup_viewport_camera = False
|
settings.should_setup_viewport_camera = False
|
||||||
|
|||||||
@@ -45,9 +45,23 @@ class Nest(bonsai.core.tool.Nest):
|
|||||||
related_object = tool.Ifc.get_entity(related_obj)
|
related_object = tool.Ifc.get_entity(related_obj)
|
||||||
if not relating_object or not related_object:
|
if not relating_object or not related_object:
|
||||||
return False
|
return False
|
||||||
if relating_object.is_a("IfcElement") and related_object.is_a("IfcElement"):
|
if relating_object == related_object:
|
||||||
return True
|
return False
|
||||||
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
|
@classmethod
|
||||||
def disable_editing(cls, obj: bpy.types.Object) -> None:
|
def disable_editing(cls, obj: bpy.types.Object) -> None:
|
||||||
|
|||||||
@@ -93,38 +93,16 @@ class Root(bonsai.core.tool.Root):
|
|||||||
elif dest.is_a("IfcTypeProduct"):
|
elif dest.is_a("IfcTypeProduct"):
|
||||||
if not source.RepresentationMaps:
|
if not source.RepresentationMaps:
|
||||||
return copied_entities
|
return copied_entities
|
||||||
|
dest.RepresentationMaps = [
|
||||||
# Copy representation maps while preserving mapped representation structures
|
ifcopenshell.util.element.copy_deep(
|
||||||
new_maps = []
|
tool.Ifc.get(),
|
||||||
for i, rep_map in enumerate(source.RepresentationMaps):
|
m,
|
||||||
source_rep = rep_map.MappedRepresentation
|
exclude=["IfcGeometricRepresentationContext"],
|
||||||
|
exclude_callback=exclude_callback,
|
||||||
# Copy the map itself
|
copied_entities=copied_entities,
|
||||||
new_map = ifcopenshell.util.element.copy(tool.Ifc.get(), rep_map)
|
)
|
||||||
|
for m in source.RepresentationMaps
|
||||||
# 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
|
|
||||||
return copied_entities
|
return copied_entities
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -75,16 +75,7 @@ class Type(bonsai.core.tool.Type):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_model_types(cls) -> list[ifcopenshell.entity_instance]:
|
def get_model_types(cls) -> list[ifcopenshell.entity_instance]:
|
||||||
ifc_file = tool.Ifc.get()
|
return tool.Ifc.get().by_type("IfcTypeProduct")
|
||||||
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
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_object_data(cls, obj: bpy.types.Object) -> Union[bpy.types.ID, None]:
|
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
|
undo_system
|
||||||
writing_docs
|
writing_docs
|
||||||
debugging
|
debugging
|
||||||
|
maintenance
|
||||||
ide/index
|
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}.",
|
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,
|
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,
|
UI_OT_i18n_updatetranslation_init_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import bpy
|
|||||||
|
|
||||||
# sys.path.append('C:\Program Files\Python37\Lib\site-packages')
|
# sys.path.append('C:\Program Files\Python37\Lib\site-packages')
|
||||||
import lxml.etree
|
import lxml.etree
|
||||||
from bspy import Gbxml # pyright: ignore[reportMissingImports]
|
from bspy import ( # ty: ignore[unresolved-import]
|
||||||
|
Gbxml, # pyright: ignore[reportMissingImports]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GbxmlExporter:
|
class GbxmlExporter:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
from math import pi
|
from math import pi
|
||||||
from pathlib import Path
|
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
|
||||||
import ifcopenshell.api.material
|
import ifcopenshell.api.material
|
||||||
import ifcopenshell.api.project
|
import ifcopenshell.api.project
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import ifcopenshell.api.spatial
|
|||||||
import ifcopenshell.api.unit
|
import ifcopenshell.api.unit
|
||||||
import ifcopenshell.guid
|
import ifcopenshell.guid
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pymeshlab # pyright: ignore[reportMissingImports]
|
import pymeshlab # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||||
|
|
||||||
|
|
||||||
class Obj2Ifc:
|
class Obj2Ifc:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import ifcopenshell.api.spatial
|
|||||||
import ifcopenshell.api.unit
|
import ifcopenshell.api.unit
|
||||||
import ifcopenshell.guid
|
import ifcopenshell.guid
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pywavefront # pyright: ignore[reportMissingImports]
|
import pywavefront # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||||
|
|
||||||
|
|
||||||
class Obj2Ifc:
|
class Obj2Ifc:
|
||||||
|
|||||||
@@ -385,6 +385,51 @@ Scenario: Edit text - change literal
|
|||||||
When I click "Edit Text"
|
When I click "Edit Text"
|
||||||
Then I see "Hello World"
|
Then I see "Hello World"
|
||||||
|
|
||||||
|
Scenario: Add text literal
|
||||||
|
Given an empty IFC project
|
||||||
|
And I add a cube
|
||||||
|
And the object "Cube" is selected
|
||||||
|
And I save IFC project
|
||||||
|
And I look at the "Drawings" panel
|
||||||
|
And I click "IMPORT"
|
||||||
|
And I click "ADD"
|
||||||
|
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
|
||||||
|
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
|
||||||
|
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
|
||||||
|
And I press "bim.add_annotation"
|
||||||
|
And the object "IfcAnnotation/TEXT" is selected
|
||||||
|
And I look at the "BIM_PT_text" panel
|
||||||
|
And I click "Enable Editing Text"
|
||||||
|
And I click the "ADD" after the text "Literals:"
|
||||||
|
And I set the "2nd Literal" property to "New Literal"
|
||||||
|
When I click "Edit Text"
|
||||||
|
Then I see "New Literal"
|
||||||
|
|
||||||
|
Scenario: Remove text literal
|
||||||
|
Given an empty IFC project
|
||||||
|
And I add a cube
|
||||||
|
And the object "Cube" is selected
|
||||||
|
And I save IFC project
|
||||||
|
And I look at the "Drawings" panel
|
||||||
|
And I click "IMPORT"
|
||||||
|
And I click "ADD"
|
||||||
|
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
|
||||||
|
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
|
||||||
|
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
|
||||||
|
And I press "bim.add_annotation"
|
||||||
|
And the object "IfcAnnotation/TEXT" is selected
|
||||||
|
And I look at the "BIM_PT_text" panel
|
||||||
|
And I click "Enable Editing Text"
|
||||||
|
And I set the "Literal" property to "Keep This"
|
||||||
|
And I click the "ADD" after the text "Literals:"
|
||||||
|
And I set the "2nd Literal" property to "Remove This"
|
||||||
|
And I click "Edit Text"
|
||||||
|
And I click "Enable Editing Text"
|
||||||
|
When I click the "2nd" "X"
|
||||||
|
And I click "Edit Text"
|
||||||
|
Then I see "Keep This"
|
||||||
|
And I don't see "Remove This"
|
||||||
|
|
||||||
Scenario: Add reference image
|
Scenario: Add reference image
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
And I save IFC project
|
And I save IFC project
|
||||||
|
|||||||
@@ -422,6 +422,24 @@ Scenario: Enable editing material set item
|
|||||||
When I press "bim.enable_editing_material_set_item(material_set_item={material_profile})"
|
When I press "bim.enable_editing_material_set_item(material_set_item={material_profile})"
|
||||||
Then nothing happens
|
Then nothing happens
|
||||||
|
|
||||||
|
Scenario: Edit layer item defaults null IsVentilated to FALSE in UI
|
||||||
|
Given an empty IFC project
|
||||||
|
And I add a cube
|
||||||
|
And the object "Cube" is selected
|
||||||
|
And I look at the "Class" panel
|
||||||
|
And I set the "Products" property to "IfcElement"
|
||||||
|
And I set the "Class" property to "IfcWall"
|
||||||
|
And I click "Assign IFC Class"
|
||||||
|
And I press "bim.add_material()"
|
||||||
|
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||||
|
And I press "bim.assign_material"
|
||||||
|
And I press "bim.enable_editing_assigned_material"
|
||||||
|
And the variable "layer" is "{ifc}.by_type('IfcMaterialLayer')[0].id()"
|
||||||
|
And I press "bim.enable_editing_material_set_item(material_set_item={layer})"
|
||||||
|
When I evaluate expression "attrs = bpy.context.active_object.BIMObjectMaterialProperties.material_set_item_attributes; is_vent = next(a for a in attrs if a.name == 'IsVentilated'); assert is_vent.enum_value == 'FALSE'; assert is_vent.is_null is True"
|
||||||
|
And I press "bim.edit_material_set_item(material_set_item={layer})"
|
||||||
|
Then I evaluate expression "assert {ifc}.by_id({layer}).IsVentilated is None"
|
||||||
|
|
||||||
Scenario: Add material set layer
|
Scenario: Add material set layer
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
And I add a cube
|
And I add a cube
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ class TemplateListItemSpy(PanelSpy):
|
|||||||
self.spied_props: list[dict[str, Any]] = []
|
self.spied_props: list[dict[str, Any]] = []
|
||||||
self.spied_operators: list[dict[str, Any]] = []
|
self.spied_operators: list[dict[str, Any]] = []
|
||||||
if len(signature(blender_panel.draw_item).parameters) == 8:
|
if len(signature(blender_panel.draw_item).parameters) == 8:
|
||||||
blender_panel.draw_item(
|
blender_panel.draw_item( # ty:ignore[missing-argument]
|
||||||
self,
|
self,
|
||||||
bpy.context,
|
bpy.context,
|
||||||
self,
|
self,
|
||||||
@@ -610,8 +610,9 @@ def i_see_the_prop_property_is_value(prop, value):
|
|||||||
@then(parsers.parse('I set the "{prop}" property to "{value}"'))
|
@then(parsers.parse('I set the "{prop}" property to "{value}"'))
|
||||||
def i_set_the_prop_property_to_value(prop: str, value: str):
|
def i_set_the_prop_property_to_value(prop: str, value: str):
|
||||||
"""
|
"""
|
||||||
:param prop: Could be either property name, property text, property icon
|
:param prop: Could be either property name, property text, property icon,
|
||||||
or property index (e.g. "1st", "2nd", "5th").
|
property index (e.g. "1st", "2nd", "5th"), or Nth named property
|
||||||
|
(e.g. "2nd Literal" for the 2nd property called "Literal").
|
||||||
:param value:
|
:param value:
|
||||||
For boolean propeties - 'TRUE' or 'FALSE'.
|
For boolean propeties - 'TRUE' or 'FALSE'.
|
||||||
"""
|
"""
|
||||||
@@ -619,12 +620,28 @@ def i_set_the_prop_property_to_value(prop: str, value: str):
|
|||||||
assert panel_spy
|
assert panel_spy
|
||||||
panel_spy.refresh_spy()
|
panel_spy.refresh_spy()
|
||||||
is_nth = False
|
is_nth = False
|
||||||
if prop[0].isnumeric() and prop.endswith(("st", "nd", "th")):
|
is_nth_named = False
|
||||||
|
nth_target = 0
|
||||||
|
prop_name = prop
|
||||||
|
if " " in prop and prop[0].isnumeric():
|
||||||
|
parts = prop.split(" ", 1)
|
||||||
|
if parts[0].endswith(("st", "nd", "th")):
|
||||||
|
is_nth_named = True
|
||||||
|
nth_target = int(parts[0][:-2]) - 1
|
||||||
|
prop_name = parts[1]
|
||||||
|
elif prop[0].isnumeric() and prop.endswith(("st", "nd", "th")):
|
||||||
is_nth = True
|
is_nth = True
|
||||||
|
named_count = 0
|
||||||
for nth, spied_prop in enumerate(panel_spy.spied_props):
|
for nth, spied_prop in enumerate(panel_spy.spied_props):
|
||||||
if is_nth and nth != int(prop[:-2]) - 1:
|
if is_nth and nth != int(prop[:-2]) - 1:
|
||||||
continue
|
continue
|
||||||
if not is_nth and prop not in (spied_prop["name"], spied_prop["text"], spied_prop["icon"]):
|
if is_nth_named:
|
||||||
|
if prop_name not in (spied_prop["name"], spied_prop["text"], spied_prop["icon"]):
|
||||||
|
continue
|
||||||
|
if named_count != nth_target:
|
||||||
|
named_count += 1
|
||||||
|
continue
|
||||||
|
elif not is_nth and prop not in (spied_prop["name"], spied_prop["text"], spied_prop["icon"]):
|
||||||
continue
|
continue
|
||||||
if spied_prop["prop_type"] == "BOOLEAN":
|
if spied_prop["prop_type"] == "BOOLEAN":
|
||||||
if value == "TRUE":
|
if value == "TRUE":
|
||||||
@@ -873,6 +890,29 @@ def i_click_button(button):
|
|||||||
_i_click_button_on_panel(button, panel_spy)
|
_i_click_button_on_panel(button, panel_spy)
|
||||||
|
|
||||||
|
|
||||||
|
@given(parsers.parse('I click the "{nth}" "{button}"'))
|
||||||
|
@when(parsers.parse('I click the "{nth}" "{button}"'))
|
||||||
|
@then(parsers.parse('I click the "{nth}" "{button}"'))
|
||||||
|
def i_click_the_nth_button(nth, button):
|
||||||
|
"""
|
||||||
|
:param nth: Ordinal like "1st", "2nd", "3rd" to select the Nth matching button.
|
||||||
|
:param button: The text or icon of the button to click.
|
||||||
|
"""
|
||||||
|
assert panel_spy
|
||||||
|
panel_spy.refresh_spy()
|
||||||
|
target = int(nth[:-2]) - 1
|
||||||
|
count = 0
|
||||||
|
for spied_operator in panel_spy.spied_operators:
|
||||||
|
if spied_operator["text"] == button or spied_operator["icon"] == button:
|
||||||
|
if count == target:
|
||||||
|
spied_operator["operator"]("INVOKE_DEFAULT", **spied_operator["kwargs"])
|
||||||
|
panel_spy.is_spy_dirty = True
|
||||||
|
return
|
||||||
|
count += 1
|
||||||
|
debug = "\n".join([f"{i} {v}" for i, v in enumerate(panel_spy.spied_operators)])
|
||||||
|
assert False, f"Could not find {nth} {button}:\n{debug}"
|
||||||
|
|
||||||
|
|
||||||
@given(parsers.parse('I click the "{button}" after the text "{text}"'))
|
@given(parsers.parse('I click the "{button}" after the text "{text}"'))
|
||||||
@when(parsers.parse('I click the "{button}" after the text "{text}"'))
|
@when(parsers.parse('I click the "{button}" after the text "{text}"'))
|
||||||
@then(parsers.parse('I click the "{button}" after the text "{text}"'))
|
@then(parsers.parse('I click the "{button}" after the text "{text}"'))
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ class TestDisableEditingText:
|
|||||||
|
|
||||||
class TestEditText:
|
class TestEditText:
|
||||||
def test_run(self, drawing):
|
def test_run(self, drawing):
|
||||||
drawing.synchronise_ifc_and_text_attributes("obj").should_be_called()
|
drawing.export_text_literal_attributes("obj").should_be_called().will_return("literal_attributes")
|
||||||
drawing.update_text_size_pset("obj").should_be_called()
|
drawing.export_font_size("obj").should_be_called().will_return("font_size")
|
||||||
drawing.update_text_annotation_properties("obj").should_be_called()
|
drawing.edit_text_font_size("obj", "font_size").should_be_called()
|
||||||
|
drawing.export_wrap_length("obj").should_be_called().will_return("wrap_length")
|
||||||
|
drawing.edit_text_wrap_length("obj", "wrap_length").should_be_called()
|
||||||
|
drawing.export_symbol("obj").should_be_called().will_return("symbol")
|
||||||
|
drawing.edit_text_symbol("obj", "symbol").should_be_called()
|
||||||
|
drawing.edit_text_literals("obj", "literal_attributes").should_be_called()
|
||||||
drawing.disable_editing_text("obj").should_be_called()
|
drawing.disable_editing_text("obj").should_be_called()
|
||||||
subject.edit_text(drawing, obj="obj")
|
subject.edit_text(drawing, obj="obj")
|
||||||
|
|
||||||
@@ -466,6 +471,7 @@ class TestRemoveDrawing:
|
|||||||
class TestUpdateDrawingName:
|
class TestUpdateDrawingName:
|
||||||
def test_do_not_update_if_name_unchanged(self, ifc, drawing):
|
def test_do_not_update_if_name_unchanged(self, ifc, drawing):
|
||||||
drawing.get_name("drawing").should_be_called().will_return("name")
|
drawing.get_name("drawing").should_be_called().will_return("name")
|
||||||
|
drawing.set_camera_name("drawing", "name").should_be_called()
|
||||||
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
|
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
|
||||||
drawing.get_name("group").should_be_called().will_return("name")
|
drawing.get_name("group").should_be_called().will_return("name")
|
||||||
drawing.get_drawing_collection("drawing").should_be_called().will_return("collection")
|
drawing.get_drawing_collection("drawing").should_be_called().will_return("collection")
|
||||||
@@ -482,6 +488,7 @@ class TestUpdateDrawingName:
|
|||||||
def test_run(self, ifc, drawing):
|
def test_run(self, ifc, drawing):
|
||||||
drawing.get_name("drawing").should_be_called().will_return("oldname")
|
drawing.get_name("drawing").should_be_called().will_return("oldname")
|
||||||
ifc.run("attribute.edit_attributes", product="drawing", attributes={"Name": "name"}).should_be_called()
|
ifc.run("attribute.edit_attributes", product="drawing", attributes={"Name": "name"}).should_be_called()
|
||||||
|
drawing.set_camera_name("drawing", "name").should_be_called()
|
||||||
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
|
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
|
||||||
drawing.get_name("group").should_be_called().will_return("oldname")
|
drawing.get_name("group").should_be_called().will_return("oldname")
|
||||||
ifc.run("attribute.edit_attributes", product="group", attributes={"Name": "name"}).should_be_called()
|
ifc.run("attribute.edit_attributes", product="group", attributes={"Name": "name"}).should_be_called()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from test.core.bootstrap import georeference, ifc
|
|||||||
class TestAddGeoreferencing:
|
class TestAddGeoreferencing:
|
||||||
def test_run(self, georeference):
|
def test_run(self, georeference):
|
||||||
georeference.add_georeferencing().should_be_called()
|
georeference.add_georeferencing().should_be_called()
|
||||||
|
georeference.set_model_origin().should_be_called()
|
||||||
subject.add_georeferencing(georeference)
|
subject.add_georeferencing(georeference)
|
||||||
|
|
||||||
|
|
||||||
@@ -35,9 +36,10 @@ class TestEnableEditingGeoreferencing:
|
|||||||
|
|
||||||
|
|
||||||
class TestRemoveGeoreferencing:
|
class TestRemoveGeoreferencing:
|
||||||
def test_run(self, ifc):
|
def test_run(self, ifc, georeference):
|
||||||
ifc.run("georeference.remove_georeferencing").should_be_called()
|
ifc.run("georeference.remove_georeferencing").should_be_called()
|
||||||
subject.remove_georeferencing(ifc)
|
georeference.set_model_origin().should_be_called()
|
||||||
|
subject.remove_georeferencing(ifc, georeference)
|
||||||
|
|
||||||
|
|
||||||
class TestDisableEditingGeoreferencing:
|
class TestDisableEditingGeoreferencing:
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ from test.core.bootstrap import geometry, ifc, model, type
|
|||||||
|
|
||||||
class TestAssignType:
|
class TestAssignType:
|
||||||
def test_assigning_and_switching_to_an_existing_type_data(self, ifc, model, type):
|
def test_assigning_and_switching_to_an_existing_type_data(self, ifc, model, type):
|
||||||
|
type.record_material_usage_attributes("element").should_be_called().will_return(None)
|
||||||
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
|
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
|
||||||
type.has_material_usage("element").should_be_called().will_return(False)
|
model.get_usage_type("type").should_be_called(2).will_return(None)
|
||||||
ifc.get_object("type").should_be_called().will_return("type_obj")
|
ifc.get_object("type").should_be_called().will_return("type_obj")
|
||||||
type.get_object_data("type_obj").should_be_called().will_return("type_obj_data")
|
type.get_object_data("type_obj").should_be_called().will_return("type_obj_data")
|
||||||
type.change_object_data("obj", "type_obj_data", is_global=False).should_be_called()
|
type.change_object_data("obj", "type_obj_data", is_global=False).should_be_called()
|
||||||
@@ -31,9 +32,10 @@ class TestAssignType:
|
|||||||
type.disable_editing("obj").should_be_called()
|
type.disable_editing("obj").should_be_called()
|
||||||
subject.assign_type(ifc, model, type, element="element", type="type")
|
subject.assign_type(ifc, model, type, element="element", type="type")
|
||||||
|
|
||||||
def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, type):
|
def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, model, type):
|
||||||
|
type.record_material_usage_attributes("element").should_be_called().will_return(None)
|
||||||
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
|
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
|
||||||
type.has_material_usage("element").should_be_called().will_return(False)
|
model.get_usage_type("type").should_be_called(2).will_return(None)
|
||||||
ifc.get_object("type").should_be_called().will_return("type_obj")
|
ifc.get_object("type").should_be_called().will_return("type_obj")
|
||||||
type.get_object_data("type_obj").should_be_called().will_return(None)
|
type.get_object_data("type_obj").should_be_called().will_return(None)
|
||||||
ifc.get_object("element").should_be_called().will_return("obj")
|
ifc.get_object("element").should_be_called().will_return("obj")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
|
import ifcopenshell.api.aggregate
|
||||||
import ifcopenshell.api.context
|
import ifcopenshell.api.context
|
||||||
import ifcopenshell.api.geometry
|
import ifcopenshell.api.geometry
|
||||||
import ifcopenshell.api.root
|
import ifcopenshell.api.root
|
||||||
@@ -99,6 +100,42 @@ class TestCanAggregate(NewFile):
|
|||||||
subelement_obj = bpy.data.objects.new("Object", None)
|
subelement_obj = bpy.data.objects.new("Object", None)
|
||||||
assert subject.can_aggregate(element_obj, subelement_obj) is False
|
assert subject.can_aggregate(element_obj, subelement_obj) is False
|
||||||
|
|
||||||
|
def test_element_cannot_aggregate_to_itself(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
element = ifc.createIfcElementAssembly()
|
||||||
|
element_obj = bpy.data.objects.new("Object", None)
|
||||||
|
tool.Ifc.link(element, element_obj)
|
||||||
|
assert subject.can_aggregate(element_obj, element_obj) is False
|
||||||
|
|
||||||
|
def test_cyclic_aggregation_is_prevented(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
assembly_a = ifc.createIfcElementAssembly()
|
||||||
|
assembly_a_obj = bpy.data.objects.new("AssemblyA", None)
|
||||||
|
tool.Ifc.link(assembly_a, assembly_a_obj)
|
||||||
|
beam = ifc.createIfcBeam()
|
||||||
|
beam_obj = bpy.data.objects.new("Beam", None)
|
||||||
|
tool.Ifc.link(beam, beam_obj)
|
||||||
|
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly_a)
|
||||||
|
assert subject.can_aggregate(beam_obj, assembly_a_obj) is False
|
||||||
|
|
||||||
|
def test_deep_cyclic_aggregation_is_prevented(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
assembly_a = ifc.createIfcElementAssembly()
|
||||||
|
assembly_a_obj = bpy.data.objects.new("AssemblyA", None)
|
||||||
|
tool.Ifc.link(assembly_a, assembly_a_obj)
|
||||||
|
assembly_b = ifc.createIfcElementAssembly()
|
||||||
|
assembly_b_obj = bpy.data.objects.new("AssemblyB", None)
|
||||||
|
tool.Ifc.link(assembly_b, assembly_b_obj)
|
||||||
|
beam = ifc.createIfcBeam()
|
||||||
|
beam_obj = bpy.data.objects.new("Beam", None)
|
||||||
|
tool.Ifc.link(beam, beam_obj)
|
||||||
|
ifcopenshell.api.aggregate.assign_object(ifc, products=[assembly_b], relating_object=assembly_a)
|
||||||
|
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly_b)
|
||||||
|
assert subject.can_aggregate(beam_obj, assembly_a_obj) is False
|
||||||
|
|
||||||
|
|
||||||
class TestHasPhysicalBodyRepresentation(NewFile):
|
class TestHasPhysicalBodyRepresentation(NewFile):
|
||||||
def test_run(self):
|
def test_run(self):
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class TestAddClassificationReferenceFromBSDD(NewFile):
|
|||||||
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
||||||
obj = bpy.data.objects["Cube"]
|
obj = bpy.data.objects["Cube"]
|
||||||
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
|
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
|
||||||
|
tool.Blender.set_active_object(obj)
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
assert element
|
assert element
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ class TestAddClassificationReferenceFromBSDD(NewFile):
|
|||||||
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
||||||
obj = bpy.data.objects["Cube"]
|
obj = bpy.data.objects["Cube"]
|
||||||
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
|
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
|
||||||
|
tool.Blender.set_active_object(obj)
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
assert element
|
assert element
|
||||||
|
|
||||||
@@ -110,6 +112,7 @@ class TestAddClassificationReferenceFromBSDD(NewFile):
|
|||||||
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
||||||
obj = bpy.data.objects["Cube"]
|
obj = bpy.data.objects["Cube"]
|
||||||
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
|
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
|
||||||
|
tool.Blender.set_active_object(obj)
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
assert element
|
assert element
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ class TestStairCalculatedParams(NewFile):
|
|||||||
pset_data = pset_data_base.copy()
|
pset_data = pset_data_base.copy()
|
||||||
calculated_data = calculated_data_base.copy()
|
calculated_data = calculated_data_base.copy()
|
||||||
pset_data["custom_first_last_tread_run"] = (0.1, 0.4)
|
pset_data["custom_first_last_tread_run"] = (0.1, 0.4)
|
||||||
|
pset_data["custom_tread_lock"] = False
|
||||||
calculated_data["Length"] += -0.2 + 0.1
|
calculated_data["Length"] += -0.2 + 0.1
|
||||||
self.compare_data(pset_data, calculated_data)
|
self.compare_data(pset_data, calculated_data)
|
||||||
|
|
||||||
@@ -183,6 +184,7 @@ class TestStairCalculatedParams(NewFile):
|
|||||||
pset_data = pset_data_base.copy()
|
pset_data = pset_data_base.copy()
|
||||||
calculated_data = calculated_data_base.copy()
|
calculated_data = calculated_data_base.copy()
|
||||||
pset_data["custom_first_last_tread_run"] = (0.0, None)
|
pset_data["custom_first_last_tread_run"] = (0.0, None)
|
||||||
|
pset_data["custom_tread_lock"] = False
|
||||||
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
||||||
self.compare_data(pset_data, calculated_data)
|
self.compare_data(pset_data, calculated_data)
|
||||||
|
|
||||||
@@ -190,6 +192,7 @@ class TestStairCalculatedParams(NewFile):
|
|||||||
pset_data = pset_data_base.copy()
|
pset_data = pset_data_base.copy()
|
||||||
calculated_data = calculated_data_base.copy()
|
calculated_data = calculated_data_base.copy()
|
||||||
pset_data["custom_first_last_tread_run"] = (None, 0.0)
|
pset_data["custom_first_last_tread_run"] = (None, 0.0)
|
||||||
|
pset_data["custom_tread_lock"] = False
|
||||||
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
||||||
self.compare_data(pset_data, calculated_data)
|
self.compare_data(pset_data, calculated_data)
|
||||||
|
|
||||||
@@ -197,6 +200,7 @@ class TestStairCalculatedParams(NewFile):
|
|||||||
pset_data = pset_data_base.copy()
|
pset_data = pset_data_base.copy()
|
||||||
calculated_data = calculated_data_base.copy()
|
calculated_data = calculated_data_base.copy()
|
||||||
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
|
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
|
||||||
|
pset_data["custom_tread_lock"] = False
|
||||||
calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
|
calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
|
||||||
self.compare_data(pset_data, calculated_data)
|
self.compare_data(pset_data, calculated_data)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api
|
import ifcopenshell.api
|
||||||
|
import ifcopenshell.api.nest
|
||||||
import ifcopenshell.api.spatial
|
import ifcopenshell.api.spatial
|
||||||
|
|
||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
@@ -51,6 +52,42 @@ class TestCanNest(NewFile):
|
|||||||
subelement_obj = bpy.data.objects.new("Object", None)
|
subelement_obj = bpy.data.objects.new("Object", None)
|
||||||
assert subject.can_nest(element_obj, subelement_obj) is False
|
assert subject.can_nest(element_obj, subelement_obj) is False
|
||||||
|
|
||||||
|
def test_element_cannot_nest_to_itself(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
element_obj = bpy.data.objects.new("Object", None)
|
||||||
|
tool.Ifc.link(element, element_obj)
|
||||||
|
assert subject.can_nest(element_obj, element_obj) is False
|
||||||
|
|
||||||
|
def test_cyclic_nesting_is_prevented(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
wall_a = ifc.createIfcWall()
|
||||||
|
wall_a_obj = bpy.data.objects.new("WallA", None)
|
||||||
|
tool.Ifc.link(wall_a, wall_a_obj)
|
||||||
|
wall_b = ifc.createIfcWall()
|
||||||
|
wall_b_obj = bpy.data.objects.new("WallB", None)
|
||||||
|
tool.Ifc.link(wall_b, wall_b_obj)
|
||||||
|
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_b], relating_object=wall_a)
|
||||||
|
assert subject.can_nest(wall_b_obj, wall_a_obj) is False
|
||||||
|
|
||||||
|
def test_deep_cyclic_nesting_is_prevented(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
wall_a = ifc.createIfcWall()
|
||||||
|
wall_a_obj = bpy.data.objects.new("WallA", None)
|
||||||
|
tool.Ifc.link(wall_a, wall_a_obj)
|
||||||
|
wall_b = ifc.createIfcWall()
|
||||||
|
wall_b_obj = bpy.data.objects.new("WallB", None)
|
||||||
|
tool.Ifc.link(wall_b, wall_b_obj)
|
||||||
|
wall_c = ifc.createIfcWall()
|
||||||
|
wall_c_obj = bpy.data.objects.new("WallC", None)
|
||||||
|
tool.Ifc.link(wall_c, wall_c_obj)
|
||||||
|
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_b], relating_object=wall_a)
|
||||||
|
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_c], relating_object=wall_b)
|
||||||
|
assert subject.can_nest(wall_c_obj, wall_a_obj) is False
|
||||||
|
|
||||||
|
|
||||||
class TestDisableEditing(NewFile):
|
class TestDisableEditing(NewFile):
|
||||||
def test_run(self):
|
def test_run(self):
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ class TestLoadingIfcSqlite(NewFile):
|
|||||||
sql_type="SQLite",
|
sql_type="SQLite",
|
||||||
)
|
)
|
||||||
patcher.patch()
|
patcher.patch()
|
||||||
tmp_file = Path(tempfile.mktemp(suffix=".ifcsqlite"))
|
tmp_file = Path(tempfile.mkstemp(suffix=".ifcsqlite")[1])
|
||||||
ifcpatch.write(patcher.get_output(), tmp_file)
|
ifcpatch.write(patcher.get_output(), tmp_file)
|
||||||
|
|
||||||
elements_with_meshes = [
|
elements_with_meshes = [
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
aiohttp
|
||||||
|
beautifulsoup4
|
||||||
|
boto3
|
||||||
|
botocore
|
||||||
|
brickschema
|
||||||
|
cjio >=0.8, <0.10
|
||||||
|
debugpy
|
||||||
|
ezdxf
|
||||||
|
fake-bpy-module-latest
|
||||||
|
git+https://github.com/prochitecture/bpypolyskel
|
||||||
|
git+https://github.com/Andrej730/IFC2JSON_python.git@pyproject_toml
|
||||||
|
gitpython
|
||||||
|
isodate
|
||||||
|
lark
|
||||||
|
lxml
|
||||||
|
lxml-stubs
|
||||||
|
markdown-it-py
|
||||||
|
natsort
|
||||||
|
numpy
|
||||||
|
odfpy
|
||||||
|
openpyxl
|
||||||
|
pandas
|
||||||
|
pillow
|
||||||
|
platformdirs
|
||||||
|
pygments
|
||||||
|
pyradiance
|
||||||
|
pystache
|
||||||
|
pytest
|
||||||
|
pytest_bdd
|
||||||
|
pytest_blender
|
||||||
|
python-dateutil
|
||||||
|
python-socketio
|
||||||
|
pytz
|
||||||
|
rdflib
|
||||||
|
requests
|
||||||
|
shapely
|
||||||
|
svgwrite
|
||||||
|
typing-extensions
|
||||||
|
typst
|
||||||
|
tzfpy
|
||||||
|
xsdata
|
||||||
@@ -7,7 +7,7 @@ from typing import Literal, Optional
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
|
||||||
|
|
||||||
from .type_hints import *
|
from type_hints import *
|
||||||
|
|
||||||
|
|
||||||
def _lower_first(s: str) -> str:
|
def _lower_first(s: str) -> str:
|
||||||
|
|||||||
@@ -955,7 +955,7 @@ def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str
|
|||||||
return val(unit.Currency)
|
return val(unit.Currency)
|
||||||
|
|
||||||
|
|
||||||
def get_unit_name(ifc_file: ifcopenshell.entity_instance, unit: ifcopenshell.entity_instance) -> Union[str, None]:
|
def get_unit_name(unit: ifcopenshell.entity_instance) -> Union[str, None]:
|
||||||
if unit.is_a("IfcNamedUnit"):
|
if unit.is_a("IfcNamedUnit"):
|
||||||
return val(unit.Name)
|
return val(unit.Name)
|
||||||
|
|
||||||
|
|||||||
@@ -953,7 +953,7 @@ def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str
|
|||||||
return val(unit.Currency)
|
return val(unit.Currency)
|
||||||
|
|
||||||
|
|
||||||
def get_unit_name(ifc_file: ifcopenshell.entity_instance, unit: ifcopenshell.entity_instance) -> Union[str, None]:
|
def get_unit_name(unit: ifcopenshell.entity_instance) -> Union[str, None]:
|
||||||
if unit.is_a("IfcNamedUnit"):
|
if unit.is_a("IfcNamedUnit"):
|
||||||
return val(unit.Name)
|
return val(unit.Name)
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst)
|
|||||||
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
|
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Eigen::Vector3d o, axis(0, 0, 1), refDirection;
|
||||||
|
|
||||||
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
|
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
|
||||||
Eigen::Vector3d o = m->components().col(3).head<3>();
|
o = m->components().col(3).head<3>();
|
||||||
|
|
||||||
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
|
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
|
||||||
// 1) Axis is given but not RefDirection
|
// 1) Axis is given but not RefDirection
|
||||||
@@ -38,12 +40,43 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst)
|
|||||||
// 3) Neither Axis or RefDirection are provided
|
// 3) Neither Axis or RefDirection are provided
|
||||||
// 4) Both Axis and RefDirection are provided
|
// 4) Both Axis and RefDirection are provided
|
||||||
|
|
||||||
Eigen::Vector3d z = inst->Axis() ? *taxonomy::cast<taxonomy::direction3>(map(inst->Axis()))->components_ : Eigen::Vector3d(0,0,1); // Axis is (0,0,1) when omitted
|
const bool hasAxis = inst->Axis() != nullptr;
|
||||||
Eigen::Vector3d rd = inst->RefDirection() ? *taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()))->components_ : m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
const bool hasRef = inst->RefDirection() != nullptr;
|
||||||
Eigen::Vector3d y = z.cross(rd);
|
|
||||||
Eigen::Vector3d x = y.cross(z);
|
|
||||||
|
|
||||||
return taxonomy::make<taxonomy::matrix4>(o, z, x);
|
/*
|
||||||
|
if (hasAxis != hasRef) {
|
||||||
|
Logger::Warning("Axis and RefDirection should be specified together", inst);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (hasAxis && !hasRef) {
|
||||||
|
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
||||||
|
axis = *a->components_;
|
||||||
|
|
||||||
|
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
||||||
|
// refDirection is not necessarily orthogonal to axis.
|
||||||
|
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
|
||||||
|
refDirection = axis.cross(refDirection).cross(axis);
|
||||||
|
} else if (!hasAxis && hasRef) {
|
||||||
|
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
||||||
|
refDirection = *r->components_;
|
||||||
|
Eigen::Vector3d up(0, 0, 1);
|
||||||
|
axis = refDirection.cross(up.cross(refDirection));
|
||||||
|
} else if (!hasAxis && !hasRef) {
|
||||||
|
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
||||||
|
Eigen::Vector3d up(0, 0, 1);
|
||||||
|
axis = refDirection.cross(up.cross(refDirection));
|
||||||
|
} else {
|
||||||
|
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
||||||
|
axis = *a->components_;
|
||||||
|
|
||||||
|
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
||||||
|
refDirection = *r->components_;
|
||||||
|
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
|
||||||
|
}
|
||||||
|
|
||||||
|
// axis and refDirection need to be orthogonal
|
||||||
|
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -27,29 +27,15 @@ SED:=sed -i '' -e
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# TODO: we should simplify this at some point...
|
SUPPORTED_PYVERSIONS := py310 py311 py312 py313 py314
|
||||||
ifeq ($(PYVERSION), py39)
|
|
||||||
PYNUMBER:=39
|
ifeq ($(filter $(PYVERSION),$(SUPPORTED_PYVERSIONS)),)
|
||||||
endif
|
$(error Unsupported PYVERSION=$(PYVERSION). Must be one of $(SUPPORTED_PYVERSIONS))
|
||||||
ifeq ($(PYVERSION), py310)
|
|
||||||
PYNUMBER:=310
|
|
||||||
endif
|
|
||||||
ifeq ($(PYVERSION), py311)
|
|
||||||
PYNUMBER:=311
|
|
||||||
endif
|
|
||||||
ifeq ($(PYVERSION), py312)
|
|
||||||
PYNUMBER:=312
|
|
||||||
endif
|
|
||||||
ifeq ($(PYVERSION), py313)
|
|
||||||
PYNUMBER:=313
|
|
||||||
endif
|
|
||||||
ifeq ($(PYVERSION), py314)
|
|
||||||
PYNUMBER:=314
|
|
||||||
endif
|
|
||||||
ifndef PYNUMBER
|
|
||||||
$(error Unsupported PYVERSION '$(PYVERSION)')
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
PYMINOR:=$(subst py3,,$(PYVERSION))
|
||||||
|
PYNUMBER:=3$(PYMINOR)
|
||||||
|
|
||||||
# We actually do support glibc 2.28-2.30 (see #5636)
|
# We actually do support glibc 2.28-2.30 (see #5636)
|
||||||
# but those are old and there's no demand for it.
|
# but those are old and there's no demand for it.
|
||||||
ifeq ($(PLATFORM), linux64)
|
ifeq ($(PLATFORM), linux64)
|
||||||
@@ -93,10 +79,6 @@ test-parallel:
|
|||||||
test-mathutils:
|
test-mathutils:
|
||||||
pytest -p no:pytest-blender test/util/test_shape_builder.py
|
pytest -p no:pytest-blender test/util/test_shape_builder.py
|
||||||
|
|
||||||
.PHONY: build-ids-docs
|
|
||||||
build-ids-docs:
|
|
||||||
mkdir -p test/build
|
|
||||||
cd test && python ids_doc_generator.py
|
|
||||||
|
|
||||||
.PHONY: qa
|
.PHONY: qa
|
||||||
qa:
|
qa:
|
||||||
|
|||||||
@@ -257,7 +257,8 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
|
|||||||
"``join({{separator}}, {{values}})``", "``join(""-"", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated."
|
"``join({{separator}}, {{values}})``", "``join(""-"", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated."
|
||||||
"``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions."
|
"``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions."
|
||||||
|
|
||||||
When using queries in an IfcAnnotation tag surround with backticks.
|
When using queries in an IfcAnnotation tag surround with backticks. Examples:
|
||||||
Examples:
|
|
||||||
````number({{Qto_WallBaseQuantities.Width}}, ",",".")```` or
|
- ````number({{Qto_WallBaseQuantities.Width}}, ",",".")````
|
||||||
````round({{Qto_BuildingElementProxyQuantities.NetVolume}},.1)````
|
- ````round({{Qto_BuildingElementProxyQuantities.NetVolume}},.1)````
|
||||||
|
- ````join(", OVER ", reverse({{material.item.Material.Name}}))````
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .stream import stream, stream_entity
|
from .stream import stream, stream_entity # ty: ignore[possibly-missing-import]
|
||||||
from .stream import stream as _stream
|
from .stream import stream as _stream # ty: ignore[possibly-missing-import]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -199,11 +199,13 @@ def open(
|
|||||||
for ty in bypass_types:
|
for ty in bypass_types:
|
||||||
f.bypass_type(ty)
|
f.bypass_type(ty)
|
||||||
if mmap:
|
if mmap:
|
||||||
f.initialize(str(path.absolute()), mmap=mmap)
|
# mmap parameter is only available for builds with USE_MMAP, not used in our main builds
|
||||||
|
f.initialize(str(path.absolute()), mmap=mmap) # type: ignore[unknown-argument]
|
||||||
else:
|
else:
|
||||||
f.initialize(str(path.absolute()))
|
f.initialize(str(path.absolute()))
|
||||||
elif mmap:
|
elif mmap:
|
||||||
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap)
|
# mmap parameter is only available for builds with USE_MMAP, not used in our main builds
|
||||||
|
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap) # type: ignore[unknown-argument]
|
||||||
else:
|
else:
|
||||||
f = ifcopenshell_wrapper.open(str(path.absolute()))
|
f = ifcopenshell_wrapper.open(str(path.absolute()))
|
||||||
return file(f)
|
return file(f)
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import importlib
|
|||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import partial
|
|
||||||
from typing import TYPE_CHECKING, Any, Optional
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@@ -90,11 +89,7 @@ def renamed_arguments_deprecation(
|
|||||||
# "group.add_group": partial(
|
# "group.add_group": partial(
|
||||||
# renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
|
# renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
|
||||||
# ),
|
# ),
|
||||||
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {
|
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {}
|
||||||
"control.assign_control": partial(
|
|
||||||
batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
CACHED_USECASE_CLASSES: dict[str, Callable] = {}
|
CACHED_USECASE_CLASSES: dict[str, Callable] = {}
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ def remove_context(file: ifcopenshell.file, context: ifcopenshell.entity_instanc
|
|||||||
new = context.ParentContext
|
new = context.ParentContext
|
||||||
for inverse in file.get_inverse(context):
|
for inverse in file.get_inverse(context):
|
||||||
if inverse.is_a("IfcCoordinateOperation"):
|
if inverse.is_a("IfcCoordinateOperation"):
|
||||||
|
# Trick to make sure the coordinate operation is not referenced
|
||||||
|
# by a context so we can delete it safely
|
||||||
inverse.SourceCRS = inverse.TargetCRS
|
inverse.SourceCRS = inverse.TargetCRS
|
||||||
ifcopenshell.util.element.remove_deep(file, inverse)
|
ifcopenshell.util.element.remove_deep2(file, inverse)
|
||||||
else:
|
else:
|
||||||
ifcopenshell.util.element.replace_attribute(inverse, context, new)
|
ifcopenshell.util.element.replace_attribute(inverse, context, new)
|
||||||
file.remove(context)
|
file.remove(context)
|
||||||
|
|||||||
@@ -59,6 +59,6 @@ def edit_cost_value(
|
|||||||
value["ValueComponent"],
|
value["ValueComponent"],
|
||||||
)
|
)
|
||||||
value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
|
value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
|
||||||
if old_unit_basis and file.get_total_inverses(old_unit_basis) == 0:
|
if old_unit_basis:
|
||||||
ifcopenshell.util.element.remove_deep(file, old_unit_basis)
|
ifcopenshell.util.element.remove_deep2(file, old_unit_basis)
|
||||||
setattr(cost_value, name, value)
|
setattr(cost_value, name, value)
|
||||||
|
|||||||
@@ -31,17 +31,6 @@ def add_boolean(
|
|||||||
) -> list[ifcopenshell.entity_instance]:
|
) -> list[ifcopenshell.entity_instance]:
|
||||||
"""Adds a boolean operation to two or more representation items
|
"""Adds a boolean operation to two or more representation items
|
||||||
|
|
||||||
If an IfcBooleanOperand is part of the top level items in an
|
|
||||||
IfcShapeRepresentation, it will be removed from that level whilst being
|
|
||||||
added to the IfcBooleanResult. This is because it is generally intuitive
|
|
||||||
that an item is either participating in a boolean operation, or being an
|
|
||||||
item in its own right, but not both.
|
|
||||||
|
|
||||||
However, if an IfcBooleanOperand is part of another boolean operation
|
|
||||||
already, it will not be removed from the existing operation. A new
|
|
||||||
operation will be created, and therefore it will participate in two
|
|
||||||
operations.
|
|
||||||
|
|
||||||
This function protects against recursive booleans.
|
This function protects against recursive booleans.
|
||||||
|
|
||||||
After a boolean operation is made, since the items of
|
After a boolean operation is made, since the items of
|
||||||
@@ -101,9 +90,6 @@ def add_boolean(
|
|||||||
|
|
||||||
booleans = []
|
booleans = []
|
||||||
for second_item in second_items:
|
for second_item in second_items:
|
||||||
for inverse in file.get_inverse(second_item):
|
|
||||||
if inverse.is_a("IfcShapeRepresentation"):
|
|
||||||
inverse.Items = list(set(inverse.Items) - {second_item})
|
|
||||||
if first.is_a("IfcTesselatedFaceSet"):
|
if first.is_a("IfcTesselatedFaceSet"):
|
||||||
first.Closed = True # For now, trust the user to do the right thing.
|
first.Closed = True # For now, trust the user to do the right thing.
|
||||||
if second_item.is_a("IfcTesselatedFaceSet"):
|
if second_item.is_a("IfcTesselatedFaceSet"):
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||||
|
|
||||||
import bmesh # pyright: ignore[reportMissingImports]
|
import bmesh # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||||
import bpy # pyright: ignore[reportMissingImports]
|
import bpy # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports]
|
from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||||
|
|
||||||
import ifcopenshell.util.shape_builder
|
import ifcopenshell.util.shape_builder
|
||||||
import ifcopenshell.util.unit
|
import ifcopenshell.util.unit
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ def validate_type(
|
|||||||
|
|
||||||
if remaining_items:
|
if remaining_items:
|
||||||
ifcopenshell.api.geometry.add_boolean(file, preferred_item, remaining_items, "UNION")
|
ifcopenshell.api.geometry.add_boolean(file, preferred_item, remaining_items, "UNION")
|
||||||
|
representation.Items = [i for i in representation.Items if i not in remaining_items]
|
||||||
|
|
||||||
representation.RepresentationType = ifcopenshell.util.representation.guess_type(representation.Items)
|
representation.RepresentationType = ifcopenshell.util.representation.guess_type(representation.Items)
|
||||||
if representation.RepresentationType == "CSG":
|
if representation.RepresentationType == "CSG":
|
||||||
|
|||||||
@@ -42,7 +42,5 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance
|
|||||||
ifcopenshell.api.grid.remove_grid_axis(model, axis=axis_2)
|
ifcopenshell.api.grid.remove_grid_axis(model, axis=axis_2)
|
||||||
"""
|
"""
|
||||||
axis_curve = axis.AxisCurve
|
axis_curve = axis.AxisCurve
|
||||||
if file.get_total_inverses(axis_curve) == 1:
|
|
||||||
ifcopenshell.util.element.remove_deep(file, axis_curve)
|
|
||||||
file.remove(axis_curve)
|
|
||||||
file.remove(axis)
|
file.remove(axis)
|
||||||
|
ifcopenshell.util.element.remove_deep2(file, axis_curve)
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
|
import ifcopenshell.api.aggregate
|
||||||
import ifcopenshell.api.owner
|
import ifcopenshell.api.owner
|
||||||
|
import ifcopenshell.api.spatial
|
||||||
import ifcopenshell.guid
|
import ifcopenshell.guid
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
|
|
||||||
@@ -137,7 +139,10 @@ def assign_object(
|
|||||||
if not objects_to_change:
|
if not objects_to_change:
|
||||||
return is_nested_by
|
return is_nested_by
|
||||||
|
|
||||||
# NOTE: An object can both be nested and assigned to a container or an aggregate.
|
# Can be either only nested, aggregated, or contained at the same time.
|
||||||
|
possibly_contained = [o for o in objects_without_nests if hasattr(o, "ContainedInStructure")]
|
||||||
|
ifcopenshell.api.spatial.unassign_container(file, products=possibly_contained)
|
||||||
|
ifcopenshell.api.aggregate.unassign_object(file, products=objects_without_nests)
|
||||||
|
|
||||||
# unassign elements from previous nests
|
# unassign elements from previous nests
|
||||||
for nests in previous_nests_rels:
|
for nests in previous_nests_rels:
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ class Usecase:
|
|||||||
)
|
)
|
||||||
ifcopenshell.api.type.assign_type(
|
ifcopenshell.api.type.assign_type(
|
||||||
self.file,
|
self.file,
|
||||||
should_run_listeners=False,
|
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||||
related_objects=[element],
|
related_objects=[element],
|
||||||
relating_type=new_type,
|
relating_type=new_type,
|
||||||
should_map_representations=False,
|
should_map_representations=False,
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ def create_file(version: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4") -> ifcope
|
|||||||
"""
|
"""
|
||||||
file = ifcopenshell.file(schema=version)
|
file = ifcopenshell.file(schema=version)
|
||||||
file.header.file_name.name = "/dev/null" # Hehehe
|
file.header.file_name.name = "/dev/null" # Hehehe
|
||||||
file.header.file_name.time_stamp = (
|
file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
|
||||||
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat()
|
|
||||||
)
|
|
||||||
file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
||||||
file.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version)
|
file.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version)
|
||||||
file.header.file_name.authorization = "Nobody"
|
file.header.file_name.authorization = "Nobody"
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import ifcopenshell.util.element
|
|||||||
def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.entity_instance) -> None:
|
def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.entity_instance) -> None:
|
||||||
"""Removes a property template
|
"""Removes a property template
|
||||||
|
|
||||||
Note that a property set template should always have at least one
|
Note that a property set template should always have at least one property
|
||||||
property template to be valid, so take care when removing property
|
template to be valid. So a property set template will not be removed if it
|
||||||
templates.
|
is the only template ina a property ste template.
|
||||||
|
|
||||||
:param prop_template: The IfcSimplePropertyTemplate to remove.
|
:param prop_template: The IfcSimplePropertyTemplate to remove.
|
||||||
:return: None
|
:return: None
|
||||||
@@ -43,10 +43,8 @@ def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.en
|
|||||||
ifcopenshell.api.pset_template.remove_prop_template(model, prop_template=prop2)
|
ifcopenshell.api.pset_template.remove_prop_template(model, prop_template=prop2)
|
||||||
"""
|
"""
|
||||||
for inverse in file.get_inverse(prop_template):
|
for inverse in file.get_inverse(prop_template):
|
||||||
if len(inverse.HasPropertyTemplates) == 1:
|
if len(inverse.HasPropertyTemplates) > 1:
|
||||||
inverse.HasPropertyTemplates = []
|
|
||||||
else:
|
|
||||||
has_property_templates = list(inverse.HasPropertyTemplates)
|
has_property_templates = list(inverse.HasPropertyTemplates)
|
||||||
has_property_templates.remove(prop_template)
|
has_property_templates.remove(prop_template)
|
||||||
inverse.HasPropertyTemplates = has_property_templates
|
inverse.HasPropertyTemplates = has_property_templates
|
||||||
ifcopenshell.util.element.remove_deep(file, prop_template)
|
ifcopenshell.util.element.remove_deep2(file, prop_template)
|
||||||
|
|||||||
@@ -38,4 +38,4 @@ def remove_pset_template(file: ifcopenshell.file, pset_template: ifcopenshell.en
|
|||||||
# Let's remove the template.
|
# Let's remove the template.
|
||||||
ifcopenshell.api.pset_template.remove_pset_template(model, pset_template=template)
|
ifcopenshell.api.pset_template.remove_pset_template(model, pset_template=template)
|
||||||
"""
|
"""
|
||||||
ifcopenshell.util.element.remove_deep(file, pset_template)
|
ifcopenshell.util.element.remove_deep2(file, pset_template)
|
||||||
|
|||||||
@@ -79,5 +79,5 @@ def add_resource_quantity(
|
|||||||
old_quantity = resource.BaseQuantity
|
old_quantity = resource.BaseQuantity
|
||||||
resource.BaseQuantity = quantity
|
resource.BaseQuantity = quantity
|
||||||
if old_quantity:
|
if old_quantity:
|
||||||
ifcopenshell.util.element.remove_deep(file, old_quantity)
|
ifcopenshell.util.element.remove_deep2(file, old_quantity)
|
||||||
return quantity
|
return quantity
|
||||||
|
|||||||
@@ -47,4 +47,4 @@ def remove_resource_quantity(file: ifcopenshell.file, resource: ifcopenshell.ent
|
|||||||
old_quantity = resource.BaseQuantity
|
old_quantity = resource.BaseQuantity
|
||||||
resource.BaseQuantity = None
|
resource.BaseQuantity = None
|
||||||
if old_quantity:
|
if old_quantity:
|
||||||
ifcopenshell.util.element.remove_deep(file, old_quantity)
|
ifcopenshell.util.element.remove_deep2(file, old_quantity)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Any, Optional
|
|||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import bpy # pyright: ignore[reportMissingImports]
|
import bpy # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
|
||||||
|
|
||||||
|
|
||||||
def add_surface_textures(
|
def add_surface_textures(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user