Merge branch 'v0.8.0' into ifcmax/initial-refresh

This commit is contained in:
Josef Wienerroither
2026-03-22 09:20:00 +01:00
141 changed files with 1730 additions and 796 deletions
+7 -4
View File
@@ -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",
+1 -1
View File
@@ -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",
+1
View File
@@ -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
View File
@@ -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
+19 -10
View File
@@ -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}")
+3
View File
@@ -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
View File
@@ -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"
+3 -3
View File
@@ -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",
# { # {
+14 -10
View File
@@ -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
View File
@@ -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
+1 -3
View File
@@ -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()
+8 -5
View File
@@ -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,
+4 -9
View File
@@ -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(
+2 -2
View File
@@ -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()
+1 -1
View File
@@ -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()
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -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):
+29 -138
View File
@@ -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,
) )
+40 -16
View File
@@ -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(
+2 -2
View File
@@ -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,
+198 -99
View File
@@ -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"}
+1 -1
View File
@@ -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()
-2
View File
@@ -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"
+1 -4
View File
@@ -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:
+1 -43
View File
@@ -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
+9
View File
@@ -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
+28 -9
View File
@@ -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:
+6 -3
View File
@@ -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
+1 -1
View File
@@ -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
+8
View File
@@ -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
+4 -3
View File
@@ -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)
+6 -4
View File
@@ -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
+1 -1
View File
@@ -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
+17 -3
View File
@@ -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:
+10 -32
View File
@@ -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
+1 -10
View File
@@ -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)
+23
View File
@@ -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"])
+2 -2
View File
@@ -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,
) )
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
+45 -5
View File
@@ -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}"'))
+10 -3
View File
@@ -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()
+4 -2
View File
@@ -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:
+5 -3
View File
@@ -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")
+37
View File
@@ -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
+4
View File
@@ -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)
+37
View File
@@ -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):
+1 -1
View File
@@ -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 = [
+41
View File
@@ -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
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+7 -25
View File
@@ -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