mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 10:33:20 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
@@ -53,6 +53,11 @@ jobs:
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1,
|
||||
}
|
||||
exclude:
|
||||
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
|
||||
- pyver: py313
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
|
||||
@@ -53,7 +53,6 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
|
||||
pip install src/bcf --no-deps
|
||||
pip install git+https://github.com/zdhoward/aud
|
||||
pip install pytest-xdist==3.8.0
|
||||
|
||||
- name: Install C++ dependencies
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ dependencies = [
|
||||
"black==26.1.0",
|
||||
"ruff==0.14.14",
|
||||
"poethepoet",
|
||||
"gersemi==0.25.1",
|
||||
"gersemi==0.25.4",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Build Bonsai .zip.
|
||||
# Usage:
|
||||
# # While in `bonsai` folder.
|
||||
# docker build -t bonsai-dist .
|
||||
# # Output zip file will be in `dist` folder.
|
||||
# # See possible values for variables in `Makefile`'s `dist` target description.
|
||||
# docker run --rm -v "$PWD/../../:/work" -w /work -e PLATFORM=win -e PYVERSION=py313 bonsai-dist
|
||||
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y make git wget unzip zip
|
||||
|
||||
# Python
|
||||
RUN apt-get install -y software-properties-common
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y python3.11 python3.11-venv
|
||||
|
||||
RUN apt-get install -y npm
|
||||
|
||||
WORKDIR /work
|
||||
CMD cd src/bonsai && make dist PLATFORM=$PLATFORM PYVERSION=$PYVERSION
|
||||
+40
-20
@@ -48,18 +48,33 @@ VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
|
||||
VERSION_DATE:=$(shell date '+%y%m%d')
|
||||
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
|
||||
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
|
||||
PYVERSION:=py310
|
||||
PYPI_IMP:=cp
|
||||
|
||||
ifeq ($(PYVERSION), py311)
|
||||
PYLIBDIR:=python3.11
|
||||
PYNUMBER:=311
|
||||
PYPI_VERSION:=3.11
|
||||
ifdef PYVERSION
|
||||
SUPPORTED_PYVERSIONS := py311 py312 py313
|
||||
|
||||
ifeq ($(filter $(PYVERSION),$(SUPPORTED_PYVERSIONS)),)
|
||||
$(error Unsupported PYVERSION=$(PYVERSION). Must be one of $(SUPPORTED_PYVERSIONS))
|
||||
endif
|
||||
|
||||
PYMINOR:=$(subst py3,,$(PYVERSION))
|
||||
PYLIBDIR:=python3.$(PYMINOR)
|
||||
PYNUMBER:=3$(PYMINOR)
|
||||
PYPI_VERSION:=3.$(PYMINOR)
|
||||
endif # def PYVERSION
|
||||
|
||||
|
||||
ifdef PLATFORM
|
||||
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
||||
|
||||
ifeq ($(filter $(PLATFORM),$(SUPPORTED_PLATFORMS)),)
|
||||
$(error Unsupported PLATFORM=$(PLATFORM). Must be one of $(SUPPORTED_PLATFORMS))
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM),macos)
|
||||
ifeq ($(PYVERSION),py313)
|
||||
$(error Blender 5.1 with Python 3.13 doesn't support intel macOS.)
|
||||
endif
|
||||
ifeq ($(PYVERSION), py312)
|
||||
PYLIBDIR:=python3.12
|
||||
PYNUMBER:=312
|
||||
PYPI_VERSION:=3.12
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), linux)
|
||||
@@ -86,6 +101,8 @@ PYPI_PLATFORM:=--platform win_amd64
|
||||
BLENDER_PLATFORM:=windows-x64
|
||||
endif
|
||||
|
||||
endif # def PLATFORM
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=e8eb5e4
|
||||
.PHONY: bump
|
||||
@@ -99,7 +116,10 @@ endif
|
||||
.PHONY: dist
|
||||
dist:
|
||||
ifndef PLATFORM
|
||||
$(error PLATFORM is not set)
|
||||
$(error PLATFORM is not set. Example values: $(SUPPORTED_PLATFORMS).)
|
||||
endif
|
||||
ifndef PYVERSION
|
||||
$(error PYVERSION is not set. Example values: $(SUPPORTED_PYVERSIONS). )
|
||||
endif
|
||||
rm -rf build
|
||||
mkdir -p build
|
||||
@@ -125,9 +145,6 @@ endif
|
||||
cd ../ifc5d && make dist && mv dist/*.whl ../bonsai/build/wheels/
|
||||
cd ../ifccityjson && make dist && mv dist/*.whl ../bonsai/build/wheels/
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download GitPython --dest=./wheels
|
||||
# Provides audio playback for costing
|
||||
# This is a REALLY IMPORTANT feature
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel git+https://github.com/zdhoward/aud --wheel-dir=./wheels
|
||||
# IfcOpenShell dependency - support for new typing features
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download typing_extensions --dest=./wheels
|
||||
# Required by IfcCSV
|
||||
@@ -189,7 +206,7 @@ endif
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download tzfpy $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
# pyradiance is using different platform versions than defaults in our makefile.
|
||||
ifeq ($(PLATFORM), linux)
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform manylinux_2_35_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform manylinux_2_28_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
else ifeq ($(PLATFORM), macos)
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform macosx_10_13_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
else
|
||||
@@ -267,6 +284,11 @@ else
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
||||
endif
|
||||
|
||||
# Blender 5.1+ requires Python 3.13.
|
||||
ifeq ($(PYVERSION), py313)
|
||||
$(SED) 's/blender_version_min = "4\.2\.0"/blender_version_min = "5.1.0"/' build/bonsai/blender_manifest.toml
|
||||
endif
|
||||
|
||||
$(SED) "s/os-arch/$(BLENDER_PLATFORM)/" build/bonsai/blender_manifest.toml
|
||||
|
||||
# Provides bonsai Add-on functionality
|
||||
@@ -288,12 +310,6 @@ endif
|
||||
fi
|
||||
mv build/wheels/*.whl build/bonsai/wheels/
|
||||
|
||||
# Temporary workaround for Blender not handling non-3.11 wheels #5743.
|
||||
# Use '-n' as on macos x86-64 doesn't have a binary wheel and it autoincludes 'cp311'.
|
||||
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
|
||||
whl_name=$$(echo $$prev_whl_name | sed "s/-cp39-/-cp$(PYNUMBER)-/"); \
|
||||
mv --update=none "$$prev_whl_name" "$$whl_name";
|
||||
|
||||
ifneq ($(PLATFORM), linux)
|
||||
# Safeguard: in case one of `pip download` will break,
|
||||
# it will produce a linux wheel for non-linux build (our github action machine is using linux).
|
||||
@@ -336,7 +352,11 @@ test:
|
||||
|
||||
.PHONY: test-core
|
||||
test-core:
|
||||
ifndef MODULE
|
||||
pytest -p no:pytest-blender test/core
|
||||
else
|
||||
pytest -p no:pytest-blender test/core/test_${MODULE}.py
|
||||
endif
|
||||
|
||||
.PHONY: test-bim
|
||||
test-bim:
|
||||
|
||||
@@ -224,7 +224,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Collector,
|
||||
tool.Spatial,
|
||||
container=current_container,
|
||||
element_obj=aggregate,
|
||||
objs=[aggregate],
|
||||
)
|
||||
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ classes = (
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
operator.ConvertSVGToDXF,
|
||||
operator.CopyTextToSelection,
|
||||
operator.CreateDrawing,
|
||||
operator.CreateSheets,
|
||||
operator.DisableAddAnnotationType,
|
||||
@@ -115,7 +116,6 @@ classes = (
|
||||
prop.BIMCameraProperties,
|
||||
prop.ElementValueRow,
|
||||
prop.LiteralProps,
|
||||
prop.LiteralApplySettings,
|
||||
prop.BIMTextProperties,
|
||||
prop.BIMAssignedProductProperties,
|
||||
prop.BIMAnnotationProperties,
|
||||
|
||||
@@ -355,8 +355,6 @@ class DecoratorData:
|
||||
font_size = FONT_SIZES[font_size_type]
|
||||
symbol = tool.Drawing.get_annotation_symbol(element)
|
||||
newline_at = pset_data.get("Newline_At", 0)
|
||||
reverse_list = pset_data.get("Reverse_List", False)
|
||||
list_separator = pset_data.get("List_Separator") or ", "
|
||||
|
||||
# other attributes
|
||||
literals = tool.Drawing.get_text_literal(obj, return_list=True)
|
||||
@@ -384,8 +382,6 @@ class DecoratorData:
|
||||
"FontSize": font_size,
|
||||
"Symbol": symbol,
|
||||
"Newline_At": newline_at,
|
||||
"Reverse_List": reverse_list,
|
||||
"List_Separator": list_separator,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1762,40 +1762,34 @@ class CutDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def cache_camera_matrix(self):
|
||||
def cache_camera_matrix(self) -> None:
|
||||
assert bpy.context.scene and bpy.context.scene.camera
|
||||
obj = bpy.context.scene.camera
|
||||
# Explicit `dtype` for Blender <5.0 compatibility.
|
||||
DecoratorData.camera_location_checksum = repr(
|
||||
np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()
|
||||
tool.Blender.np_array_legacy(obj.matrix_world.translation).tobytes()
|
||||
)
|
||||
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
|
||||
DecoratorData.camera_rotation_checksum = repr(tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()).tobytes())
|
||||
|
||||
def is_camera_moved(self):
|
||||
def is_camera_moved(self) -> bool:
|
||||
if not DecoratorData.camera_location_checksum:
|
||||
self.cache_camera_matrix()
|
||||
return True # Let's be conservative
|
||||
|
||||
assert bpy.context.scene and bpy.context.scene.camera
|
||||
obj = bpy.context.scene.camera
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
loc_checksum_bytes = eval(DecoratorData.camera_location_checksum)
|
||||
if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64)
|
||||
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32)
|
||||
else: # New format: 3 * 4 bytes (float32)
|
||||
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32)
|
||||
|
||||
loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten()
|
||||
loc_checksum_bytes: bytes = eval(DecoratorData.camera_location_checksum)
|
||||
loc_check = tool.Blender.np_frombuffer_legacy(loc_checksum_bytes, 3)
|
||||
loc_real = tool.Blender.np_array_legacy(obj.matrix_world.translation)
|
||||
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
rot_checksum_bytes = eval(DecoratorData.camera_rotation_checksum)
|
||||
if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64)
|
||||
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3)
|
||||
else: # New format: 9 * 4 bytes (float32)
|
||||
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3)
|
||||
|
||||
rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32)
|
||||
rot_checksum_bytes: bytes = eval(DecoratorData.camera_rotation_checksum)
|
||||
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9)
|
||||
rot_real = tool.Blender.np_array_legacy(obj.matrix_world.to_3x3())
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
if angle_rad > 0.0017453292519943296: # 0.1 degrees
|
||||
|
||||
@@ -3181,169 +3181,34 @@ class EditText(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_text"
|
||||
bl_label = "Edit Text"
|
||||
bl_description = "Save changes to the text annotation and\ndisable the text editing options"
|
||||
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
|
||||
captured_apply_settings = {
|
||||
"apply_font_size_to_all": props.apply_font_size_to_all,
|
||||
"apply_newline_to_all": props.apply_newline_to_all,
|
||||
"font_size": props.font_size,
|
||||
"newline_at": props.newline_at,
|
||||
"literals": [],
|
||||
}
|
||||
|
||||
for i, literal in enumerate(props.literals):
|
||||
literal_data = {
|
||||
"attributes": [
|
||||
(attr.string_value, attr.enum_value if attr.data_type == "enum" else attr.string_value)
|
||||
for attr in literal.attributes
|
||||
],
|
||||
"box_alignment": literal.box_alignment[:] if hasattr(literal, "box_alignment") else None,
|
||||
"element_value_rows": [
|
||||
{
|
||||
"category": row.category,
|
||||
"element_key": row.element_key,
|
||||
"formatted_value": row.formatted_value,
|
||||
"separator": row.separator,
|
||||
}
|
||||
for row in literal.element_value_rows
|
||||
],
|
||||
"product_used": literal.product_used.name if literal.product_used else None,
|
||||
}
|
||||
|
||||
if i < len(props.literal_apply_settings):
|
||||
apply_settings = props.literal_apply_settings[i]
|
||||
literal_data["apply_text_to_all"] = apply_settings.apply_text_to_all
|
||||
literal_data["apply_path_to_all"] = apply_settings.apply_path_to_all
|
||||
literal_data["apply_box_alignment_to_all"] = apply_settings.apply_box_alignment_to_all
|
||||
else:
|
||||
literal_data["apply_text_to_all"] = False
|
||||
literal_data["apply_path_to_all"] = False
|
||||
literal_data["apply_box_alignment_to_all"] = False
|
||||
|
||||
captured_apply_settings["literals"].append(literal_data)
|
||||
|
||||
obj["_bonsai_element_value_rows_backup"] = json.dumps(captured_apply_settings["literals"])
|
||||
|
||||
core.edit_text(tool.Drawing, obj=obj)
|
||||
|
||||
self.apply_to_selected_objects_with_captured_data(context, obj, captured_apply_settings)
|
||||
|
||||
core.edit_text(tool.Drawing, obj=tool.Blender.get_active_object())
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def apply_to_selected_objects(self, context, active_obj, active_props):
|
||||
"""Apply changes to other selected text objects based on toggle settings"""
|
||||
selected_objects = [obj for obj in context.selected_objects if obj != active_obj]
|
||||
class CopyTextToSelection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.copy_text_to_selection"
|
||||
bl_label = "Copy Text To Selection"
|
||||
bl_description = "Copy text formatting or literals to selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
attribute: bpy.props.StringProperty()
|
||||
|
||||
for obj in selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
continue
|
||||
|
||||
obj_props = tool.Drawing.get_text_props(obj)
|
||||
needs_update = False
|
||||
|
||||
if active_props.apply_font_size_to_all:
|
||||
obj_props.font_size = active_props.font_size
|
||||
needs_update = True
|
||||
|
||||
if active_props.apply_newline_to_all:
|
||||
obj_props.newline_at = active_props.newline_at
|
||||
needs_update = True
|
||||
|
||||
for i, active_literal in enumerate(active_props.literals):
|
||||
if i >= len(obj_props.literals):
|
||||
continue
|
||||
|
||||
obj_props.ensure_literal_apply_settings(len(obj_props.literals))
|
||||
obj_literal = obj_props.literals[i]
|
||||
|
||||
if i < len(active_props.literal_apply_settings):
|
||||
active_settings = active_props.literal_apply_settings[i]
|
||||
|
||||
if active_settings.apply_text_to_all:
|
||||
if len(active_literal.attributes) > 0 and len(obj_literal.attributes) > 0:
|
||||
obj_literal.attributes[0].string_value = active_literal.attributes[0].string_value
|
||||
needs_update = True
|
||||
|
||||
if active_settings.apply_path_to_all:
|
||||
if len(active_literal.attributes) > 1 and len(obj_literal.attributes) > 1:
|
||||
if (
|
||||
active_literal.attributes[1].data_type == "enum"
|
||||
and obj_literal.attributes[1].data_type == "enum"
|
||||
):
|
||||
obj_literal.attributes[1].enum_value = active_literal.attributes[1].enum_value
|
||||
else:
|
||||
obj_literal.attributes[1].string_value = active_literal.attributes[1].string_value
|
||||
needs_update = True
|
||||
|
||||
if active_settings.apply_box_alignment_to_all:
|
||||
obj_literal.box_alignment = active_literal.box_alignment[:]
|
||||
needs_update = True
|
||||
|
||||
if needs_update:
|
||||
core.edit_text(tool.Drawing, obj=obj)
|
||||
|
||||
def apply_to_selected_objects_with_captured_data(self, context, active_obj, captured_data):
|
||||
"""Apply changes to other selected text objects using captured apply settings"""
|
||||
selected_objects = [obj for obj in context.selected_objects if obj != active_obj]
|
||||
|
||||
for obj in selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
continue
|
||||
|
||||
obj_props = tool.Drawing.get_text_props(obj)
|
||||
|
||||
if len(obj_props.literals) == 0:
|
||||
core.enable_editing_text(tool.Drawing, obj=obj)
|
||||
obj_props.ensure_literal_apply_settings(len(obj_props.literals))
|
||||
|
||||
needs_update = False
|
||||
|
||||
if captured_data["apply_font_size_to_all"]:
|
||||
obj_props.font_size = captured_data["font_size"]
|
||||
needs_update = True
|
||||
|
||||
if captured_data["apply_newline_to_all"]:
|
||||
obj_props.newline_at = captured_data["newline_at"]
|
||||
needs_update = True
|
||||
|
||||
for i, captured_literal in enumerate(captured_data["literals"]):
|
||||
if i >= len(obj_props.literals):
|
||||
continue
|
||||
|
||||
obj_literal = obj_props.literals[i]
|
||||
|
||||
if captured_literal["apply_text_to_all"]:
|
||||
if len(captured_literal["attributes"]) > 0 and len(obj_literal.attributes) > 0:
|
||||
new_value = captured_literal["attributes"][0][0] # [0] = string_value
|
||||
obj_literal.attributes[0].string_value = new_value
|
||||
needs_update = True
|
||||
|
||||
if captured_literal["apply_path_to_all"]:
|
||||
if len(captured_literal["attributes"]) > 1 and len(obj_literal.attributes) > 1:
|
||||
new_value = captured_literal["attributes"][1][1] # [1] = enum_value or string_value
|
||||
if obj_literal.attributes[1].data_type == "enum":
|
||||
obj_literal.attributes[1].enum_value = new_value
|
||||
else:
|
||||
obj_literal.attributes[1].string_value = new_value
|
||||
needs_update = True
|
||||
|
||||
if captured_literal["apply_box_alignment_to_all"] and captured_literal["box_alignment"]:
|
||||
obj_literal.box_alignment = captured_literal["box_alignment"]
|
||||
needs_update = True
|
||||
|
||||
if needs_update:
|
||||
core.edit_text(tool.Drawing, obj=obj)
|
||||
def _execute(self, context):
|
||||
apply_objs = [
|
||||
obj
|
||||
for obj in tool.Blender.get_selected_objects()
|
||||
if (element := tool.Ifc.get_entity(obj))
|
||||
and tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
|
||||
]
|
||||
core.copy_text_to_selection(
|
||||
tool.Drawing,
|
||||
attribute=self.attribute,
|
||||
attribute_obj=tool.Blender.get_active_object(),
|
||||
apply_objs=apply_objs,
|
||||
)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
|
||||
class EnableEditingText(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -3358,8 +3223,6 @@ class EnableEditingText(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
core.enable_editing_text(tool.Drawing, obj=obj)
|
||||
|
||||
props.ensure_literal_apply_settings(len(props.literals))
|
||||
|
||||
text_element = tool.Ifc.get_entity(obj)
|
||||
assigned_product_entity = tool.Drawing.get_assigned_product(text_element) if text_element else None
|
||||
assigned_product_obj = tool.Ifc.get_object(assigned_product_entity) if assigned_product_entity else None
|
||||
@@ -3448,9 +3311,6 @@ class AddTextLiteral(bpy.types.Operator):
|
||||
box_alignment_mask = [False] * 9
|
||||
box_alignment_mask[6] = True # bottom_left box_alignment
|
||||
literal_props.box_alignment = box_alignment_mask
|
||||
|
||||
props.ensure_literal_apply_settings(len(props.literals))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -3469,9 +3329,6 @@ class RemoveTextLiteral(bpy.types.Operator):
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.literals.remove(self.literal_prop_id)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
props.ensure_literal_apply_settings(len(props.literals))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -5056,7 +4913,7 @@ class ShowCategoryHelp(bpy.types.Operator):
|
||||
|
||||
class AddElementValueRow(bpy.types.Operator):
|
||||
bl_idname = "bim.add_element_value_row"
|
||||
bl_label = "Add Element"
|
||||
bl_label = "Add Element Value Row"
|
||||
bl_description = "Add a new element value row"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
@@ -872,22 +872,20 @@ class LiteralProps(PropertyGroup):
|
||||
category_for_adding: str
|
||||
|
||||
|
||||
class LiteralApplySettings(PropertyGroup):
|
||||
literal_index: IntProperty(name="Literal Index")
|
||||
apply_text_to_all: BoolProperty(name="Apply Text to All", default=False)
|
||||
apply_path_to_all: BoolProperty(name="Apply Path to All", default=False)
|
||||
apply_box_alignment_to_all: BoolProperty(name="Apply Box Alignment to All", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
literal_index: int
|
||||
apply_text_to_all: bool
|
||||
apply_path_to_all: bool
|
||||
apply_box_alignment_to_all: bool
|
||||
|
||||
|
||||
class BIMTextProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
literals: CollectionProperty(name="Literals", type=LiteralProps)
|
||||
newline_at: IntProperty(name="Newline At")
|
||||
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Symbol",
|
||||
description="Symbol from symbols.svg to use for this text.",
|
||||
items=[(s, s, "") for s in ["NO SYMBOL", "CUSTOM SYMBOL"] + tool.Drawing.DEFAULT_SYMBOLS],
|
||||
default="NO SYMBOL",
|
||||
)
|
||||
custom_symbol: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Custom Symbol",
|
||||
description="Non-default symbol to use for this text.",
|
||||
)
|
||||
font_size: EnumProperty(
|
||||
items=[
|
||||
("1.8", "1.8 - Small", ""),
|
||||
@@ -899,52 +897,34 @@ class BIMTextProperties(PropertyGroup):
|
||||
default="2.5",
|
||||
name="Font Size",
|
||||
)
|
||||
newline_at: IntProperty(name="Newline At")
|
||||
reverse_list: BoolProperty(name="Reverse List", description="Reverses the order of any list.", default=False)
|
||||
list_separator: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="List Separator",
|
||||
description="Text used to separate lists. Uses a comma (, ) if empty.",
|
||||
align_horizontal: EnumProperty(
|
||||
items=[
|
||||
("left", "Left", "", "ALIGN_LEFT", 0),
|
||||
("middle", "Middle", "", "ALIGN_CENTER", 1),
|
||||
("right", "Right", "", "ALIGN_RIGHT", 2),
|
||||
],
|
||||
default="left",
|
||||
name="Horizontal Alignment",
|
||||
)
|
||||
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Symbol",
|
||||
description="Symbol from symbols.svg to use for this text.",
|
||||
items=[(s, s, "") for s in ["NO SYMBOL", "CUSTOM SYMBOL"] + tool.Drawing.DEFAULT_SYMBOLS],
|
||||
default="NO SYMBOL",
|
||||
align_vertical: EnumProperty(
|
||||
items=[
|
||||
("top", "Top", "", "ALIGN_TOP", 0),
|
||||
("middle", "Middle", "", "ALIGN_MIDDLE", 1),
|
||||
("bottom", "Bottom", "", "ALIGN_BOTTOM", 2),
|
||||
],
|
||||
default="middle",
|
||||
name="Vertical Alignment",
|
||||
)
|
||||
custom_symbol: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Custom Symbol",
|
||||
description="Non-default symbol to use for this text.",
|
||||
)
|
||||
|
||||
apply_font_size_to_all: BoolProperty(
|
||||
name="Apply Font Size to All", description="Apply font size changes to all selected text objects", default=False
|
||||
)
|
||||
apply_newline_to_all: BoolProperty(
|
||||
name="Apply Newline to All", description="Apply newline changes to all selected text objects", default=False
|
||||
)
|
||||
|
||||
literal_apply_settings: CollectionProperty(name="Literal Apply Settings", type=LiteralApplySettings)
|
||||
|
||||
def ensure_literal_apply_settings(self, literal_count: int):
|
||||
"""Ensure we have apply settings for all literals"""
|
||||
while len(self.literal_apply_settings) > literal_count:
|
||||
self.literal_apply_settings.remove(len(self.literal_apply_settings) - 1)
|
||||
|
||||
while len(self.literal_apply_settings) < literal_count:
|
||||
setting = self.literal_apply_settings.add()
|
||||
setting.literal_index = len(self.literal_apply_settings) - 1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
|
||||
font_size: str
|
||||
newline_at: int
|
||||
reverse_list: bool
|
||||
list_separator: str
|
||||
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
|
||||
custom_symbol: str
|
||||
apply_font_size_to_all: bool
|
||||
apply_newline_to_all: bool
|
||||
font_size: str
|
||||
align_horizontal: str
|
||||
align_vertical: str
|
||||
|
||||
def get_symbol(self) -> Union[str, None]:
|
||||
if self.symbol == "NO SYMBOL":
|
||||
@@ -979,8 +959,6 @@ class BIMTextProperties(PropertyGroup):
|
||||
"FontSize": float(self.font_size),
|
||||
"Newline_At": int(self.newline_at),
|
||||
"Symbol": self.get_symbol(),
|
||||
"Reverse_List": self.reverse_list,
|
||||
"List_Separator": self.list_separator or ", ",
|
||||
}
|
||||
return text_data
|
||||
|
||||
|
||||
@@ -989,11 +989,6 @@ class SvgWriter:
|
||||
symbol = tool.Drawing.get_annotation_symbol(element)
|
||||
newline_at = tool.Drawing.get_newline_at(element)
|
||||
|
||||
# Get reverse_list and list_separator from EPset_Annotation
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||
reverse_list = pset_data.get("Reverse_List", False)
|
||||
list_separator = pset_data.get("List_Separator") or ", "
|
||||
|
||||
template_text_fields = []
|
||||
if symbol:
|
||||
symbol_transform = self.get_symbol_transform(text_position_svg_str, angle, text_obj)
|
||||
@@ -1010,7 +1005,7 @@ class SvgWriter:
|
||||
# NOTE: zip makes sure that we iterate over the shortest list
|
||||
for field, text_literal in zip(template_text_fields, text_literals):
|
||||
field.text = tool.Drawing.replace_text_literal_variables(
|
||||
text_literal.Literal, product or element, reverse_list, list_separator
|
||||
text_literal.Literal, product or element
|
||||
)
|
||||
field.attrib["class"] = classes_str
|
||||
|
||||
@@ -1030,9 +1025,7 @@ class SvgWriter:
|
||||
line_number = 0
|
||||
|
||||
for text_literal in text_literals:
|
||||
text = tool.Drawing.replace_text_literal_variables(
|
||||
text_literal.Literal, product or element, reverse_list, list_separator
|
||||
)
|
||||
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element)
|
||||
|
||||
text_segments = parse_markdown_it(text)
|
||||
|
||||
|
||||
@@ -644,58 +644,56 @@ class BIM_PT_text(Panel):
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="Add Literal")
|
||||
else:
|
||||
row.operator("bim.edit_text", icon="CHECKMARK")
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="")
|
||||
row.operator("bim.disable_editing_text", icon="CANCEL", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "font_size")
|
||||
row.prop(props, "apply_font_size_to_all", text="", icon="COPYDOWN")
|
||||
row.operator("bim.copy_text_to_selection", text="", icon="COPYDOWN").attribute = "FONT_SIZE"
|
||||
row = self.layout.row()
|
||||
row.label(text="Alignment")
|
||||
row.prop(props, "align_horizontal", text="", expand=True)
|
||||
row.prop(props, "align_vertical", text="", expand=True)
|
||||
row.operator("bim.copy_text_to_selection", text="", icon="COPYDOWN").attribute = "ALIGNMENT"
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "newline_at")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "reverse_list")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "list_separator")
|
||||
row.operator("bim.copy_text_to_selection", text="", icon="COPYDOWN").attribute = "WRAP_LENGTH"
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "symbol")
|
||||
if props.symbol == "CUSTOM SYMBOL":
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "custom_symbol", text="")
|
||||
row.prop(props, "apply_newline_to_all", text="", icon="COPYDOWN")
|
||||
select_op = row.operator("bim.select_similar_text_literal_value", text="", icon="RESTRICT_SELECT_OFF")
|
||||
select_op.literal_value = str(props.newline_at)
|
||||
select_op.attribute_type = "newline"
|
||||
row.operator("bim.copy_text_to_selection", text="", icon="COPYDOWN").attribute = "SYMBOL"
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Literals:")
|
||||
row.operator("bim.copy_text_to_selection", text="", icon="COPYDOWN").attribute = "LITERALS"
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="")
|
||||
|
||||
for i, literal_props in enumerate(props.literals):
|
||||
box = self.layout.box()
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Literal[{i}]:")
|
||||
if i > 0:
|
||||
row.operator("bim.order_text_literal_up", icon="TRIA_UP", text="").literal_prop_id = i
|
||||
if i < len(props.literals) - 1:
|
||||
row.operator("bim.order_text_literal_down", icon="TRIA_DOWN", text="").literal_prop_id = i
|
||||
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
|
||||
|
||||
if len(literal_props.attributes) > 0 and i < len(props.literal_apply_settings):
|
||||
if len(literal_props.attributes):
|
||||
row = box.row(align=True)
|
||||
bonsai.bim.helper.draw_attribute(literal_props.attributes[0], row, enable_search=True)
|
||||
if i > 0:
|
||||
row.operator("bim.order_text_literal_up", icon="TRIA_UP", text="").literal_prop_id = i
|
||||
if i < len(props.literals) - 1:
|
||||
row.operator("bim.order_text_literal_down", icon="TRIA_DOWN", text="").literal_prop_id = i
|
||||
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
|
||||
|
||||
expand_icon = "DOWNARROW_HLT" if getattr(literal_props, "show_element_values", False) else "RIGHTARROW"
|
||||
op = row.operator("bim.toggle_element_values_panel", icon=expand_icon, text="")
|
||||
op.literal_prop_id = i
|
||||
|
||||
row.prop(props.literal_apply_settings[i], "apply_text_to_all", text="", icon="COPYDOWN")
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assigned_element = tool.Drawing.get_assigned_product(element) or element
|
||||
resolved_value = tool.Drawing.replace_text_literal_variables(
|
||||
literal_props.attributes[0].string_value,
|
||||
assigned_element,
|
||||
props.reverse_list,
|
||||
props.list_separator,
|
||||
)
|
||||
row = box.row(align=True)
|
||||
row.label(text="CurrentValue:")
|
||||
@@ -779,8 +777,6 @@ class BIM_PT_text(Panel):
|
||||
else:
|
||||
row.prop(attr, "string_value", text="Path")
|
||||
select_value = attr.string_value
|
||||
if i < len(props.literal_apply_settings):
|
||||
row.prop(props.literal_apply_settings[i], "apply_path_to_all", text="", icon="COPYDOWN")
|
||||
|
||||
other_attributes = [a for a in literal_props.attributes[2:] if a.name != "BoxAlignment"]
|
||||
if other_attributes:
|
||||
@@ -800,10 +796,6 @@ class BIM_PT_text(Panel):
|
||||
col = row.column(align=True)
|
||||
alignment_label_row = col.row(align=True)
|
||||
alignment_label_row.label(text=" Text box alignment:")
|
||||
if i < len(props.literal_apply_settings):
|
||||
alignment_label_row.prop(
|
||||
props.literal_apply_settings[i], "apply_box_alignment_to_all", text="", icon="COPYDOWN"
|
||||
)
|
||||
|
||||
box_alignment_value = (
|
||||
literal_props.attributes[
|
||||
@@ -824,61 +816,52 @@ class BIM_PT_text(Panel):
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
|
||||
if props.is_editing:
|
||||
self.draw_text_editing_ui(context)
|
||||
else:
|
||||
text_data = DecoratorData.get_text_data(obj)
|
||||
return self.draw_text_editing_ui(context)
|
||||
text_data = DecoratorData.get_text_data(obj)
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="FontSize")
|
||||
click_op = row.operator(
|
||||
"bim.select_similar_text_literal_value", text=str(text_data["FontSize"]), emboss=False
|
||||
)
|
||||
click_op.literal_value = str(text_data["FontSize"])
|
||||
click_op.attribute_type = "font_size"
|
||||
click_op.display_text = str(text_data["FontSize"])
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="FontSize")
|
||||
click_op = row.operator("bim.select_similar_text_literal_value", text=str(text_data["FontSize"]), emboss=False)
|
||||
click_op.literal_value = str(text_data["FontSize"])
|
||||
click_op.attribute_type = "font_size"
|
||||
click_op.display_text = str(text_data["FontSize"])
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Newline_At")
|
||||
click_op = row.operator(
|
||||
"bim.select_similar_text_literal_value", text=str(text_data["Newline_At"]), emboss=False
|
||||
)
|
||||
click_op.literal_value = str(text_data["Newline_At"])
|
||||
click_op.attribute_type = "newline"
|
||||
click_op.display_text = str(text_data["Newline_At"])
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Reverse_List")
|
||||
row.label(text=str(text_data["Reverse_List"]))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="List_Separator")
|
||||
row.label(text=str(text_data["List_Separator"]))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Newline_At")
|
||||
click_op = row.operator(
|
||||
"bim.select_similar_text_literal_value", text=str(text_data["Newline_At"]), emboss=False
|
||||
)
|
||||
click_op.literal_value = str(text_data["Newline_At"])
|
||||
click_op.attribute_type = "newline"
|
||||
click_op.display_text = str(text_data["Newline_At"])
|
||||
|
||||
for i, literal_data in enumerate(text_data["Literals"]):
|
||||
box = self.layout.box()
|
||||
box.label(text=f"Literal[{i}]:")
|
||||
for i, literal_data in enumerate(text_data["Literals"]):
|
||||
box = self.layout.box()
|
||||
box.label(text=f"Literal[{i}]:")
|
||||
|
||||
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
||||
for attribute in literal_data:
|
||||
row = box.row(align=True)
|
||||
row.label(text=attribute)
|
||||
click_op = row.operator(
|
||||
"bim.select_similar_text_literal_value",
|
||||
text=str(literal_data[attribute]),
|
||||
emboss=False,
|
||||
)
|
||||
click_op.literal_value = str(literal_data[attribute])
|
||||
click_op.literal_index = i
|
||||
if attribute == "Literal":
|
||||
click_op.attribute_type = "literal"
|
||||
elif attribute == "Path":
|
||||
click_op.attribute_type = "path"
|
||||
elif attribute == "BoxAlignment":
|
||||
click_op.attribute_type = "box_alignment"
|
||||
else:
|
||||
click_op.attribute_type = "text"
|
||||
click_op.display_text = str(literal_data[attribute])
|
||||
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
||||
for attribute in literal_data:
|
||||
row = box.row(align=True)
|
||||
row.label(text=attribute)
|
||||
click_op = row.operator(
|
||||
"bim.select_similar_text_literal_value",
|
||||
text=str(literal_data[attribute]),
|
||||
emboss=False,
|
||||
)
|
||||
click_op.literal_value = str(literal_data[attribute])
|
||||
click_op.literal_index = i
|
||||
if attribute == "Literal":
|
||||
click_op.attribute_type = "literal"
|
||||
elif attribute == "Path":
|
||||
click_op.attribute_type = "path"
|
||||
elif attribute == "BoxAlignment":
|
||||
click_op.attribute_type = "box_alignment"
|
||||
else:
|
||||
click_op.attribute_type = "text"
|
||||
click_op.display_text = str(literal_data[attribute])
|
||||
|
||||
|
||||
class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
|
||||
@@ -1730,7 +1730,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Collector,
|
||||
tool.Spatial,
|
||||
container=original_data[matching_group_id][index]["Container"],
|
||||
element_obj=obj,
|
||||
objs=[obj],
|
||||
)
|
||||
for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)):
|
||||
tool.Collector.assign(tool.Ifc.get_object(part))
|
||||
|
||||
@@ -50,9 +50,10 @@ classes = (
|
||||
array.EditArray,
|
||||
array.EnableEditingArray,
|
||||
array.ApplyArray,
|
||||
array.RegenerateArray,
|
||||
array.RemoveArray,
|
||||
array.SelectArrayParent,
|
||||
array.SelectAllArrayObjects,
|
||||
array.SelectArrayParent,
|
||||
array.Input3DCursorXArray,
|
||||
array.Input3DCursorYArray,
|
||||
array.Input3DCursorZArray,
|
||||
|
||||
@@ -61,7 +61,6 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"use_local_space": True,
|
||||
"sync_children": False,
|
||||
"method": "OFFSET",
|
||||
}
|
||||
|
||||
@@ -80,28 +79,27 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset=pset,
|
||||
properties={"Parent": element.GlobalId, "Data": ifc_file.create_entity("IfcText", json.dumps(data))},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class DisableEditingArray(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_array"
|
||||
bl_label = "Disable Editing Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
tool.Model.get_array_props(obj).is_editing = -1
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingArray(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_array"
|
||||
bl_label = "Enable Editing Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
item: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -122,11 +120,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.y = data["y"] * si_conversion
|
||||
props.z = data["z"] * si_conversion
|
||||
props.use_local_space = data.get("use_local_space", False)
|
||||
props.sync_children = data.get("sync_children", False)
|
||||
props.method = data.get("method", "OFFSET")
|
||||
|
||||
props.is_editing = self.item
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -151,31 +147,25 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"y": props.y / si_conversion,
|
||||
"z": props.z / si_conversion,
|
||||
"use_local_space": props.use_local_space,
|
||||
"sync_children": props.sync_children,
|
||||
"method": props.method,
|
||||
}
|
||||
|
||||
props.is_editing = -1
|
||||
|
||||
try:
|
||||
parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"]))
|
||||
parent_element = tool.Ifc.get().by_guid(pset["Parent"])
|
||||
parent = tool.Ifc.get_object(parent_element)
|
||||
except:
|
||||
return {"FINISHED"}
|
||||
|
||||
tool.Blender.Modifier.Array.remove_constraints(parent_element)
|
||||
tool.Model.regenerate_array(parent, data)
|
||||
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
data = tool.Ifc.get().createIfcText(json.dumps(data))
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True)
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
# clears the relating_array_object so it doesn't show again next time
|
||||
props.relating_array_object = None
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ApplyArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.apply_array"
|
||||
@@ -192,6 +182,33 @@ class ApplyArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RegenerateArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.regenerate_array"
|
||||
bl_label = "Regenerate Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(pset["Parent"])
|
||||
parent = tool.Ifc.get_object(parent_element)
|
||||
except:
|
||||
return {"FINISHED"}
|
||||
pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array")
|
||||
arrays = json.loads(pset["Data"])
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
for array in arrays:
|
||||
for child in set(array["children"]):
|
||||
if child_obj := tool.Ifc.get_object(tool.Ifc.get().by_guid(child)):
|
||||
tool.Geometry.delete_ifc_object(child_obj)
|
||||
array["children"].clear()
|
||||
print("cleared array", arrays)
|
||||
tool.Model.regenerate_array(obj, arrays)
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
|
||||
class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_array"
|
||||
bl_label = "Remove Array"
|
||||
@@ -216,7 +233,8 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_editing = -1
|
||||
|
||||
try:
|
||||
parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"]))
|
||||
parent_element = tool.Ifc.get().by_guid(pset["Parent"])
|
||||
parent = tool.Ifc.get_object(parent_element)
|
||||
except:
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -226,9 +244,10 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
if not self.keep_objs:
|
||||
data[self.item]["count"] = 1
|
||||
tool.Blender.Modifier.Array.remove_constraints(parent_element)
|
||||
tool.Model.regenerate_array(parent, data, array_layers_to_apply=[self.item] if self.keep_objs else [])
|
||||
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Array")
|
||||
if len(data) == 1:
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
else:
|
||||
@@ -237,8 +256,6 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectArrayParent(bpy.types.Operator):
|
||||
bl_idname = "bim.select_array_parent"
|
||||
|
||||
@@ -470,7 +470,7 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
||||
parent = ifcopenshell.util.element.get_container(building_element)
|
||||
if parent:
|
||||
bonsai.core.spatial.assign_container(
|
||||
tool.Ifc, tool.Collector, tool.Spatial, container=parent, element_obj=obj
|
||||
tool.Ifc, tool.Collector, tool.Spatial, container=parent, objs=[obj]
|
||||
)
|
||||
|
||||
# set occurrences properties for the types defined with modifiers
|
||||
@@ -493,7 +493,7 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
if self.container_obj:
|
||||
bonsai.core.spatial.assign_container(
|
||||
tool.Ifc, tool.Collector, tool.Spatial, container=self.container, element_obj=obj
|
||||
tool.Ifc, tool.Collector, tool.Spatial, container=self.container, objs=[obj]
|
||||
)
|
||||
if props.rl_mode == "BOTTOM":
|
||||
obj.location.z = self.container_obj.location.z - tool.Blender.get_object_bounding_box(obj)["min_z"]
|
||||
|
||||
@@ -1131,8 +1131,13 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
|
||||
|
||||
MEPGenerator().setup_ports(profile1["obj"])
|
||||
else:
|
||||
connect_IfcFlowSegments = tool.Ifc.get_entity(profiles[0]["obj"]).is_a("IfcFlowSegment")
|
||||
for profile1, profile2 in zip(profiles[:-1], profiles[1:]):
|
||||
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
|
||||
if connect_IfcFlowSegments:
|
||||
bpy.ops.bim.mep_connect_elements(
|
||||
obj1_name=profile1["obj"].name, obj2_name=profile2["obj"].name
|
||||
)
|
||||
|
||||
def modal(self, context, event):
|
||||
return IfcStore.execute_ifc_operator(self, context, event, method="MODAL")
|
||||
|
||||
@@ -387,11 +387,6 @@ class BIMArrayProperties(PropertyGroup):
|
||||
name="Method",
|
||||
default="OFFSET",
|
||||
)
|
||||
sync_children: bpy.props.BoolProperty(
|
||||
name="Sync Children",
|
||||
description="Regenerate all children based on the parent object",
|
||||
default=False,
|
||||
)
|
||||
relating_array_object: bpy.props.PointerProperty(
|
||||
type=bpy.types.Object,
|
||||
name="Copy Array Properties",
|
||||
|
||||
@@ -231,17 +231,16 @@ class DumbSlabPlaner:
|
||||
for inverse in tool.Ifc.get().get_inverse(layer_set):
|
||||
if not inverse.is_a("IfcMaterialLayerSetUsage") or inverse.LayerSetDirection != "AXIS3":
|
||||
continue
|
||||
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
for rel in tool.Ifc.get().get_inverse(inverse):
|
||||
if not rel.is_a("IfcRelAssociatesMaterial"):
|
||||
continue
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, total_thickness, preserve_offset=True)
|
||||
self.change_thickness(element, total_thickness)
|
||||
else:
|
||||
for rel in inverse.AssociatedTo:
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, total_thickness, preserve_offset=True)
|
||||
self.change_thickness(element, total_thickness)
|
||||
|
||||
def regenerate_from_occurence(self, element, material_set_usage):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
@@ -251,12 +250,10 @@ class DumbSlabPlaner:
|
||||
return
|
||||
self.change_thickness(element, total_thickness)
|
||||
|
||||
def change_thickness(
|
||||
self, element: ifcopenshell.entity_instance, thickness: float, preserve_offset: bool = False
|
||||
) -> None:
|
||||
def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None:
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if tool.Model.get_usage_type(element) != "LAYER3":
|
||||
return
|
||||
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
ifc_file = tool.Ifc.get()
|
||||
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
@@ -274,48 +271,72 @@ class DumbSlabPlaner:
|
||||
if representation:
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if extrusion:
|
||||
# TODO Right now we don't have a reliable way to calculate the existing x_angle only based solely on the extrusion direction.
|
||||
# For instances, a 30 degrees angled extrusion with positive direction has the same extrusion direction as a
|
||||
# -150 degrees angled extrusion with negative direction. The difference lies in the object's rotation.
|
||||
# This means that things can get messy if the user changes the object x angle somehow. We have to figure out an alternative approach.
|
||||
existing_x_angle = obj.rotation_euler.x
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2 * pi, tolerance=0.001) else existing_x_angle
|
||||
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
offset_direction = direction_ratios.copy()
|
||||
perpendicular_depth = thickness * abs(1 / cos(existing_x_angle))
|
||||
perpendicular_offset = layer_offset * abs(1 / cos(existing_x_angle)) / self.unit_scale
|
||||
|
||||
# Calculate the actual extrusion angle from vertical
|
||||
extrusion_angle = 0
|
||||
if direction_ratios.length > 0:
|
||||
cos_angle = direction_ratios.normalized().dot(Vector((0, 0, 1)))
|
||||
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
||||
|
||||
# Only apply 1/cos factor when there's actual extrusion slope
|
||||
if extrusion_angle > 1e-6:
|
||||
perpendicular_depth = thickness * abs(1 / cos(extrusion_angle))
|
||||
perpendicular_offset = layer_offset * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
perpendicular_depth = thickness
|
||||
perpendicular_offset = layer_offset
|
||||
# Check angle and z direction to determine whether the extrusion direction is positive or negative
|
||||
if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or (
|
||||
abs(existing_x_angle) > (pi / 2) and direction_ratios.z < 0
|
||||
):
|
||||
# The extrusion direction is positive. If the layer_parameter is set to negative,
|
||||
# then the we change the extrusion direction.
|
||||
if layer_params["direction_sense"] == "NEGATIVE":
|
||||
direction_ratios *= -1
|
||||
elif (abs(existing_x_angle) > (pi / 2) and direction_ratios.z > 0) or (
|
||||
abs(existing_x_angle) < (pi / 2) and direction_ratios.z < 0
|
||||
):
|
||||
# The extrusion direction is negative. If the layer_parameter is set to positive,
|
||||
# then the we change the extrusion direction. And the offset direction should remain positive
|
||||
# for either direction sense, so we change it.
|
||||
offset_direction *= -1
|
||||
if layer_params["direction_sense"] == "POSITIVE":
|
||||
direction_ratios *= -1
|
||||
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
|
||||
extrusion.Depth = perpendicular_depth
|
||||
|
||||
# Update position
|
||||
ifc_position = extrusion.Position
|
||||
position = offset_direction * perpendicular_offset
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material:
|
||||
if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
material.OffsetFromReferenceLine = position.z
|
||||
if ifc_position:
|
||||
ifc_position.Location.Coordinates = position
|
||||
else:
|
||||
tool.Model.add_extrusion_position(extrusion, position)
|
||||
|
||||
if direction_ratios.length > 0:
|
||||
offset_vector = direction_ratios.normalized() * perpendicular_offset
|
||||
position = offset_vector
|
||||
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
# Only set offset if not preserving it (preserves independent offsets per instance)
|
||||
if not preserve_offset:
|
||||
material.OffsetFromReferenceLine = position.z
|
||||
|
||||
if ifc_position:
|
||||
ifc_position.Location.Coordinates = position
|
||||
else:
|
||||
tool.Model.add_extrusion_position(extrusion, position)
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
else:
|
||||
props = tool.Model.get_model_props()
|
||||
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
||||
new_rep = ifcopenshell.api.geometry.add_slab_representation(
|
||||
tool.Ifc.get(),
|
||||
context=body_context,
|
||||
depth=thickness * self.unit_scale,
|
||||
x_angle=x_angle,
|
||||
)
|
||||
for inverse in tool.Ifc.get().get_inverse(representation):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, representation, new_rep)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_rep,
|
||||
)
|
||||
bonsai.core.geometry.remove_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||
)
|
||||
return
|
||||
else:
|
||||
props = tool.Model.get_model_props()
|
||||
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
||||
@@ -328,118 +349,13 @@ class DumbSlabPlaner:
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=element, representation=representation
|
||||
)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
def update_extrusion_direction(
|
||||
element: ifcopenshell.entity_instance, new_direction_ratios: tuple, obj: bpy.types.Object = None
|
||||
) -> None:
|
||||
"""
|
||||
Update extrusion direction while preserving overall object orientation.
|
||||
|
||||
Args:
|
||||
element: The IFC element
|
||||
new_direction_ratios: New extrusion direction ratios (x,y,z)
|
||||
obj: Optional Blender object (will be fetched if not provided)
|
||||
"""
|
||||
if not obj:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
|
||||
# Get current extrusion direction
|
||||
old_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
if old_direction.length == 0:
|
||||
old_direction = Vector((0, 0, 1)) # Default
|
||||
|
||||
new_direction = Vector(new_direction_ratios)
|
||||
if new_direction.length == 0:
|
||||
new_direction = Vector((0, 0, 1)) # Default
|
||||
|
||||
# Normalize both directions
|
||||
old_direction_normalized = old_direction.normalized()
|
||||
new_direction_normalized = new_direction.normalized()
|
||||
|
||||
# Store current object matrix
|
||||
old_matrix = obj.matrix_world.copy()
|
||||
|
||||
# Calculate the rotation needed to keep same orientation
|
||||
# When extrusion direction changes from A to B relative to local coordinates,
|
||||
# we need to rotate the object by the inverse of that change
|
||||
|
||||
# Calculate rotation from old to new direction
|
||||
rotation_axis = old_direction_normalized.cross(new_direction_normalized)
|
||||
if rotation_axis.length > 1e-6:
|
||||
rotation_axis.normalized()
|
||||
dot_product = old_direction_normalized.dot(new_direction_normalized)
|
||||
angle = acos(min(max(dot_product, -1), 1))
|
||||
|
||||
# Apply INVERSE rotation to object to compensate
|
||||
rotation_matrix = Matrix.Rotation(-angle, 4, rotation_axis)
|
||||
|
||||
# Update object rotation
|
||||
obj.matrix_world = old_matrix @ rotation_matrix
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# Update extrusion direction (keeping magnitude)
|
||||
if old_direction.length > 0:
|
||||
# Preserve the magnitude of the original direction vector
|
||||
magnitude = old_direction.length
|
||||
new_direction = new_direction_normalized * magnitude
|
||||
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_direction)
|
||||
|
||||
# Update depth based on new extrusion angle
|
||||
extrusion_angle = 0
|
||||
if new_direction.length > 0:
|
||||
cos_angle = new_direction_normalized.dot(Vector((0, 0, 1)))
|
||||
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
||||
|
||||
# Get current depth (perpendicular depth)
|
||||
current_perpendicular_depth = extrusion.Depth
|
||||
|
||||
# If we have material layer info, calculate actual thickness
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
actual_thickness = current_perpendicular_depth
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
layer_set = material.ForLayerSet
|
||||
actual_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
actual_thickness *= unit_scale
|
||||
|
||||
# Convert to perpendicular depth if needed
|
||||
if extrusion_angle > 1e-6:
|
||||
new_perpendicular_depth = actual_thickness * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
new_perpendicular_depth = actual_thickness
|
||||
|
||||
extrusion.Depth = new_perpendicular_depth
|
||||
|
||||
# Update position offset if needed
|
||||
if extrusion.Position:
|
||||
# Recalculate offset based on new direction
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
offset = material.OffsetFromReferenceLine
|
||||
if extrusion_angle > 1e-6:
|
||||
perpendicular_offset = offset * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
perpendicular_offset = offset
|
||||
|
||||
offset_vector = new_direction_normalized * perpendicular_offset
|
||||
extrusion.Position.Location.Coordinates = tuple(offset_vector)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
|
||||
class EnableEditingSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -716,8 +632,6 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
|
||||
# TODO: review #7537 properly, this is a quick fix but something doesn't seem right.
|
||||
original_rotation_x = 0
|
||||
if extrusion.Position:
|
||||
@@ -732,49 +646,22 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
# For AXIS3 with dual rotation: Reset rotation to zero so profile is horizontal
|
||||
if usage_type == "LAYER3":
|
||||
# Store original rotation for later restoration
|
||||
original_rotation_x = obj.rotation_euler.x
|
||||
obj["pre_edit_rotation_x"] = original_rotation_x
|
||||
|
||||
# Reset rotation to zero - profile will be horizontal
|
||||
current_z_rot = obj.rotation_euler.z
|
||||
obj.rotation_euler.x = 0.0
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
else:
|
||||
# Original behavior: Restore Object rotation to zero
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(-existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
# Restore Object rotation to zero
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(-existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
# Import profile with correct x_angle
|
||||
if usage_type == "LAYER3":
|
||||
# For LAYER3: Use x_angle=0 and scale by cos(rotation) to get horizontal projection
|
||||
obj_x_rotation = original_rotation_x # Use stored original rotation
|
||||
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
||||
|
||||
# Import with x_angle=0
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=0)
|
||||
|
||||
# Scale the Y coordinates by cos(rotation) to get horizontal projection
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
for vert in obj.data.vertices:
|
||||
vert.co.y *= scale_factor
|
||||
else:
|
||||
# For other types: Use existing_x_angle
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=existing_x_angle)
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=existing_x_angle)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context))
|
||||
if not bpy.app.background:
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -796,7 +683,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
|
||||
if extrusion.Position:
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||
@@ -810,38 +696,17 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
# Restore rotation
|
||||
if usage_type == "LAYER3":
|
||||
# Restore original rotation from before editing
|
||||
if "pre_edit_rotation_x" in obj:
|
||||
current_z_rot = obj.rotation_euler.z
|
||||
obj.rotation_euler.x = obj["pre_edit_rotation_x"]
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
del obj["pre_edit_rotation_x"]
|
||||
else:
|
||||
# Original behavior
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
# Restore Object rotation to x_angle
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
# Export profile with correct x_angle
|
||||
if usage_type == "LAYER3":
|
||||
# Scale Y coordinates back up before exporting
|
||||
obj_x_rotation = obj.rotation_euler.x
|
||||
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
||||
|
||||
# Un-scale the profile before exporting
|
||||
for vert in obj.data.vertices:
|
||||
vert.co.y /= scale_factor # Inverse of import scaling
|
||||
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=0)
|
||||
else:
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
|
||||
|
||||
if not profile:
|
||||
|
||||
@@ -893,28 +758,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Ifc.get(), product=element, representation=new_footprint
|
||||
)
|
||||
|
||||
footprint_context = ifcopenshell.util.representation.get_context(
|
||||
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
if not footprint_context:
|
||||
return
|
||||
|
||||
curves = [profile.OuterCurve]
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
curves.extend(profile.InnerCurves)
|
||||
new_footprint = ifcopenshell.api.geometry.add_footprint_representation(
|
||||
tool.Ifc.get(), context=footprint_context, curves=curves
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
|
||||
if old_footprint:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_footprint):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
|
||||
bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint)
|
||||
else:
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=element, representation=new_footprint
|
||||
)
|
||||
|
||||
|
||||
class ResetVertex(bpy.types.Operator):
|
||||
bl_idname = "bim.reset_vertex"
|
||||
|
||||
@@ -229,6 +229,7 @@ class BIM_PT_array(bpy.types.Panel):
|
||||
if ArrayData.data["parameters"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF")
|
||||
row.operator("bim.regenerate_array", icon="FILE_REFRESH", text="")
|
||||
row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="")
|
||||
row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="")
|
||||
|
||||
@@ -246,7 +247,6 @@ class BIM_PT_array(bpy.types.Panel):
|
||||
row.prop(props, "method")
|
||||
row = box.row(align=True)
|
||||
row.prop(props, "use_local_space")
|
||||
row.prop(props, "sync_children")
|
||||
col = box.column()
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "x")
|
||||
|
||||
@@ -403,42 +403,24 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
continue
|
||||
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
continue
|
||||
|
||||
# Get extrusion direction
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
|
||||
# Calculate angle from vertical
|
||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
|
||||
# For sloped walls, compensate so VERTICAL height = target depth
|
||||
cos_angle = cos(x_angle)
|
||||
compensation_factor = abs(1 / cos_angle) if abs(cos_angle) > 1e-6 else 1.0
|
||||
new_depth_ifc = (self.depth / si_conversion) * compensation_factor
|
||||
|
||||
extrusion.Depth = new_depth_ifc
|
||||
|
||||
# IMPORTANT: Refresh the geometry to reflect the IFC changes
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
|
||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements":
|
||||
related_element = rel.RelatedElement
|
||||
if related_element.is_a() == "IfcWall":
|
||||
layer2_objs.append(tool.Ifc.get_object(related_element))
|
||||
ifcopenshell.api.geometry.disconnect_element(
|
||||
ifc_file,
|
||||
relating_element=rel.RelatingElement,
|
||||
related_element=element,
|
||||
)
|
||||
layer2_objs.append(obj)
|
||||
|
||||
if layer2_objs:
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -458,143 +440,81 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
layer2_objs: list[bpy.types.Object] = []
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
x_angle = 0 if tool.Cad.is_x(self.x_angle, 0, tolerance=0.001) else self.x_angle
|
||||
x_angle = 0 if tool.Cad.is_x(self.x_angle, pi, tolerance=0.001) else self.x_angle
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
x_angle = self.x_angle
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
|
||||
for obj in context.selected_objects:
|
||||
for obj in selected_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
|
||||
assert element
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
continue
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
continue
|
||||
|
||||
# Get current object rotation matrix
|
||||
obj_rotation = obj.matrix_world.to_3x3()
|
||||
|
||||
# Get current extrusion direction in LOCAL coordinates
|
||||
current_local_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
if current_local_direction.length == 0:
|
||||
current_local_direction = Vector((0, 0, 1))
|
||||
current_local_direction_normalized = current_local_direction.normalized()
|
||||
|
||||
# Calculate what the current extrusion direction is in WORLD coordinates
|
||||
current_world_direction = obj_rotation @ current_local_direction_normalized
|
||||
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
|
||||
# Calculate the NEW local extrusion direction based on x_angle
|
||||
new_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
|
||||
|
||||
# Check if extrusion direction is actually changing
|
||||
current_local_norm = current_local_direction_normalized
|
||||
new_local_norm = new_local_direction.normalized()
|
||||
|
||||
# Compare the LOCAL directions
|
||||
local_direction_changed = (new_local_norm - current_local_norm).length > 1e-6
|
||||
|
||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
depth = extrusion.Depth / abs(1 / cos(existing_x_angle))
|
||||
perpendicular_depth = depth * abs(1 / cos(x_angle))
|
||||
|
||||
# Update extrusion direction
|
||||
if local_direction_changed:
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_local_direction)
|
||||
|
||||
# Always update depth
|
||||
extrusion.Depth = perpendicular_depth
|
||||
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
|
||||
layer2_objs.append(obj)
|
||||
|
||||
extrusion.Depth = perpendicular_depth
|
||||
else:
|
||||
if tool.Model.get_usage_type(element) == "LAYER3":
|
||||
# For slabs, handle polyline scaling
|
||||
existing_obj_x_angle = obj.rotation_euler.x
|
||||
existing_obj_x_angle = (
|
||||
0 if tool.Cad.is_x(existing_obj_x_angle, 0, tolerance=0.001) else existing_obj_x_angle
|
||||
)
|
||||
existing_obj_x_angle = (
|
||||
0 if tool.Cad.is_x(existing_obj_x_angle, pi, tolerance=0.001) else existing_obj_x_angle
|
||||
)
|
||||
existing_x_angle = obj.rotation_euler.x
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
|
||||
# Scale the polyline coordinates
|
||||
coord_list = builder.get_polyline_coords(extrusion.SweptArea.OuterCurve)
|
||||
coord_list = [
|
||||
(p[0], p[1] * abs(cos(existing_x_angle))) for p in coord_list
|
||||
] # Reset the transformation
|
||||
] # Reset the transformation and returns to the original points with 0 degrees
|
||||
coord_list = [
|
||||
(p[0], p[1] * abs(1 / cos(x_angle))) for p in coord_list
|
||||
] # Apply the transformation for the new x_angle
|
||||
builder.set_polyline_coords(extrusion.SweptArea.OuterCurve, coord_list)
|
||||
|
||||
# Calculate new extrusion direction with direction sense
|
||||
base_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
|
||||
# The extrusion direction calculated previously default to the positive direction
|
||||
# Here we set the extrusion direction to negative if that's the case
|
||||
direction_ratios = Vector((0.0, sin(x_angle), cos(x_angle)))
|
||||
# direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale
|
||||
perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale
|
||||
offset_direction = base_local_direction.copy()
|
||||
offset_direction = direction_ratios.copy()
|
||||
|
||||
# Apply direction sense
|
||||
final_local_direction = base_local_direction.copy()
|
||||
if (abs(x_angle) < (pi / 2) and base_local_direction.z > 0) or (
|
||||
abs(x_angle) > (pi / 2) and base_local_direction.z < 0
|
||||
# Check angle and z direction to determine whether the extrusion direction is positive or negative
|
||||
if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or (
|
||||
abs(x_angle) > (pi / 2) and direction_ratios.z < 0
|
||||
):
|
||||
# The extrusion direction is positive. If the layer_parameter is set to negative,
|
||||
# then the we change the extrusion direction.
|
||||
if layer_params["direction_sense"] == "NEGATIVE":
|
||||
final_local_direction *= -1
|
||||
elif (x_angle > (pi / 2) and base_local_direction.z > 0) or (
|
||||
x_angle < (pi / 2) and base_local_direction.z < 0
|
||||
direction_ratios *= -1
|
||||
elif ((x_angle) > (pi / 2) and direction_ratios.z > 0) or (
|
||||
(x_angle) < (pi / 2) and direction_ratios.z < 0
|
||||
):
|
||||
# The extrusion direction is negative. If the layer_parameter is set to positive,
|
||||
# then the we change the extrusion direction.
|
||||
# then the we change the extrusion direction. And the offset direction should remain positive
|
||||
# for either direction sense, so we change it.
|
||||
offset_direction *= -1
|
||||
if layer_params["direction_sense"] == "POSITIVE":
|
||||
final_local_direction *= -1
|
||||
direction_ratios *= -1
|
||||
|
||||
# Check if extrusion direction actually changed
|
||||
final_local_norm = final_local_direction.normalized()
|
||||
local_direction_changed = (final_local_norm - current_local_norm).length > 1e-6
|
||||
|
||||
# Update extrusion properties
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(final_local_direction)
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
|
||||
extrusion.Depth = perpendicular_depth
|
||||
|
||||
if extrusion.Position or perpendicular_offset != 0:
|
||||
position = offset_direction * perpendicular_offset
|
||||
tool.Model.add_extrusion_position(extrusion, position)
|
||||
|
||||
# Adjust object rotation if extrusion direction changed
|
||||
if local_direction_changed:
|
||||
# Calculate what the NEW world direction would be with current object rotation
|
||||
expected_new_world_direction = obj_rotation @ final_local_norm
|
||||
|
||||
# The rotation needed is from expected_new_world_direction to current_world_direction
|
||||
rotation_axis = expected_new_world_direction.cross(current_world_direction)
|
||||
if rotation_axis.length > 1e-6:
|
||||
rotation_axis.normalize()
|
||||
dot_product = expected_new_world_direction.dot(current_world_direction)
|
||||
angle = acos(min(max(dot_product, -1), 1))
|
||||
|
||||
# Rotate around object's own origin
|
||||
# Decompose the matrix to get translation, rotation, scale
|
||||
translation, rotation, scale = obj.matrix_world.decompose()
|
||||
|
||||
# Create rotation matrix and convert to quaternion
|
||||
rotation_matrix = Matrix.Rotation(angle, 4, rotation_axis)
|
||||
rotation_quat = rotation_matrix.to_quaternion()
|
||||
|
||||
# Apply rotation to existing rotation (quaternion multiplication)
|
||||
new_rotation = rotation_quat @ rotation
|
||||
|
||||
# Reconstruct matrix_world with same translation, new rotation, same scale
|
||||
obj.matrix_world = (
|
||||
Matrix.Translation(translation) @ new_rotation.to_matrix().to_4x4() @ Matrix.Scale(1, 4)
|
||||
)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
@@ -602,6 +522,12 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
# Object rotation
|
||||
current_z_rot = obj.rotation_euler.z
|
||||
rot_mat = mathutils.Matrix.Rotation(x_angle, 4, "X")
|
||||
obj.rotation_euler = rot_mat.to_euler()
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
|
||||
if layer2_objs:
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
@@ -1096,7 +1022,6 @@ class DumbWallGenerator:
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
|
||||
@@ -2735,17 +2735,27 @@ class IFCFileHandlerOperator(bpy.types.Operator):
|
||||
# Keeping code in .invoke() as we'll probably add some
|
||||
# popup windows later.
|
||||
|
||||
def clean_up_path(path: str) -> str:
|
||||
# In Blender 4.5.6 there was a bug producing unncesseary double slash prefix
|
||||
# breaking the paths. Issue is not present in 5.0+ and presumably will be solved in 4.5.7 too.
|
||||
# https://projects.blender.org/blender/blender/issues/153822
|
||||
if bpy.app.version == (4, 5, 6):
|
||||
blender_prefix = "//"
|
||||
if path.startswith(blender_prefix):
|
||||
return path.removeprefix(blender_prefix)
|
||||
return path
|
||||
|
||||
# `files` contain only .ifc files.
|
||||
filepath = Path(self.directory)
|
||||
# If user is just drag'n'dropping a single file -> load it as a new project,
|
||||
# if they're holding ALT -> link the file/files to the current project.
|
||||
if event.alt:
|
||||
# Passing self.files directly results in TypeError.
|
||||
serialized_files = [{"name": f.name} for f in self.files]
|
||||
serialized_files = [{"name": clean_up_path(f.name)} for f in self.files]
|
||||
return bpy.ops.bim.link_ifc(directory=self.directory, files=serialized_files)
|
||||
else:
|
||||
if len(self.files) == 1:
|
||||
return bpy.ops.bim.load_project(filepath=(filepath / self.files[0].name).as_posix())
|
||||
return bpy.ops.bim.load_project(filepath=(filepath / clean_up_path(self.files[0].name)).as_posix())
|
||||
else:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
|
||||
@@ -370,7 +370,7 @@ class BIMProjectProperties(PropertyGroup):
|
||||
load_indexed_maps: BoolProperty(
|
||||
name="Load Indexed Maps",
|
||||
description="Load indexed maps (UV and color maps)",
|
||||
default=True,
|
||||
default=False, # Very slow and hackishly implemented
|
||||
)
|
||||
links: CollectionProperty(name="Links", type=Link)
|
||||
active_link_index: IntProperty(name="Active Link Index")
|
||||
|
||||
@@ -134,23 +134,55 @@ class IfcClassData:
|
||||
return [("FACE", "Face", "A planar face surface")]
|
||||
templates = [
|
||||
("EMPTY", "No Geometry", "Start with an empty object"),
|
||||
None,
|
||||
(
|
||||
"OBJ",
|
||||
"Tessellation From Object",
|
||||
"Use an object as a template to create a new tessellation",
|
||||
),
|
||||
(
|
||||
"MESH",
|
||||
"Custom Tessellation",
|
||||
"Create a basic tessellated or faceted cube",
|
||||
),
|
||||
(
|
||||
"EXTRUSION",
|
||||
"Custom Extruded Solid",
|
||||
"An extrusion from an arbitrary profile",
|
||||
),
|
||||
]
|
||||
|
||||
if ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcWindow"):
|
||||
templates.extend([None, ("WINDOW", "Window", "Parametric window")])
|
||||
elif ifc_class in ("IfcDoorType", "IfcDoorStyle", "IfcDoor"):
|
||||
templates.extend([None, ("DOOR", "Door", "Parametric door")])
|
||||
elif ifc_class in ("IfcStairType", "IfcStairFlightType", "IfcStair", "IfcStairFlight"):
|
||||
templates.extend([None, ("STAIR", "Stair", "Parametric stair")])
|
||||
elif ifc_class in ("IfcRailingType", "IfcRailing"):
|
||||
templates.extend([None, ("RAILING", "Railing", "Parametric railing")])
|
||||
elif ifc_class in ("IfcRoofType", "IfcRoof", "IfcSlabType", "IfcSlab", "IfcCovering", "IfcCoveringType"):
|
||||
templates.extend([None, ("ROOF", "Roof", "Parametric roof with a constant pitch")])
|
||||
elif ifc_class and "Segment" in ifc_class:
|
||||
templates.extend(
|
||||
(
|
||||
None,
|
||||
(
|
||||
"FLOW_SEGMENT_RECTANGULAR",
|
||||
"Rectangular Distribution Segment",
|
||||
"Works similarly to Profile, has distribution ports",
|
||||
),
|
||||
(
|
||||
"FLOW_SEGMENT_RECTANGULAR_HOLLOW",
|
||||
"Rectangular Hollow Distribution Segment",
|
||||
"Works similarly to Profile, has distribution ports",
|
||||
),
|
||||
(
|
||||
"FLOW_SEGMENT_CIRCULAR",
|
||||
"Circular Distribution Segment",
|
||||
"Works similarly to Profile, has distribution ports",
|
||||
),
|
||||
(
|
||||
"FLOW_SEGMENT_CIRCULAR_HOLLOW",
|
||||
"Circular Hollow Distribution Segment",
|
||||
"Works similarly to Profile, has distribution ports",
|
||||
),
|
||||
)
|
||||
)
|
||||
if ifc_class and "IfcCableCarrierSegment" in ifc_class:
|
||||
templates.extend(
|
||||
(
|
||||
(
|
||||
"FLOW_SEGMENT_U_SHAPE",
|
||||
"U-Shape Distribution Segment",
|
||||
"Uses IfcUShapeProfileDef, has distribution ports",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if ifc_class.endswith("Type") or ifc_class.endswith("Style"):
|
||||
templates.extend(
|
||||
[
|
||||
@@ -172,38 +204,26 @@ class IfcClassData:
|
||||
),
|
||||
]
|
||||
)
|
||||
if ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcWindow"):
|
||||
templates.extend([None, ("WINDOW", "Window", "Parametric window")])
|
||||
elif ifc_class in ("IfcDoorType", "IfcDoorStyle", "IfcDoor"):
|
||||
templates.extend([None, ("DOOR", "Door", "Parametric door")])
|
||||
elif ifc_class in ("IfcStairType", "IfcStairFlightType", "IfcStair", "IfcStairFlight"):
|
||||
templates.extend([None, ("STAIR", "Stair", "Parametric stair")])
|
||||
elif ifc_class in ("IfcRailingType", "IfcRailing"):
|
||||
templates.extend([None, ("RAILING", "Railing", "Parametric railing")])
|
||||
elif ifc_class in ("IfcRoofType", "IfcRoof", "IfcSlabType", "IfcSlab", "IfcCovering", "IfcCoveringType"):
|
||||
templates.extend([None, ("ROOF", "Roof", "Parametric roof with a constant pitch")])
|
||||
elif ifc_class and "Segment" in ifc_class:
|
||||
templates.extend(
|
||||
templates.extend(
|
||||
[
|
||||
None,
|
||||
(
|
||||
None,
|
||||
(
|
||||
"FLOW_SEGMENT_RECTANGULAR",
|
||||
"Rectangular Distribution Segment",
|
||||
"Works similarly to Profile, has distribution ports",
|
||||
),
|
||||
(
|
||||
"FLOW_SEGMENT_CIRCULAR",
|
||||
"Circular Distribution Segment",
|
||||
"Works similarly to Profile, has distribution ports",
|
||||
),
|
||||
(
|
||||
"FLOW_SEGMENT_CIRCULAR_HOLLOW",
|
||||
"Circular Hollow Distribution Segment",
|
||||
"Works similarly to Profile, has distribution ports",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
"OBJ",
|
||||
"Tessellation From Object",
|
||||
"Use an object as a template to create a new tessellation",
|
||||
),
|
||||
(
|
||||
"MESH",
|
||||
"Custom Tessellation",
|
||||
"Create a basic tessellated or faceted cube",
|
||||
),
|
||||
(
|
||||
"EXTRUSION",
|
||||
"Custom Extruded Solid",
|
||||
"An extrusion from an arbitrary profile",
|
||||
),
|
||||
]
|
||||
)
|
||||
return templates
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -697,6 +697,26 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
XDim=default_x_dim / unit_scale,
|
||||
YDim=default_y_dim / unit_scale,
|
||||
)
|
||||
elif representation_template == "FLOW_SEGMENT_RECTANGULAR_HOLLOW":
|
||||
default_x_dim = 0.4
|
||||
default_y_dim = 0.2
|
||||
default_thickness = 0.005
|
||||
default_inner_fillet_radius = 0.005
|
||||
default_outer_fillet_radius = 0.005
|
||||
profile_name = (
|
||||
f"{props.ifc_class}-{default_x_dim*1000}x{default_y_dim*1000}x{default_thickness*1000}"
|
||||
)
|
||||
profile = tool.Ifc.get().create_entity(
|
||||
"IfcRectangleHollowProfileDef",
|
||||
ProfileName=profile_name,
|
||||
ProfileType="AREA",
|
||||
XDim=default_x_dim / unit_scale,
|
||||
YDim=default_y_dim / unit_scale,
|
||||
WallThickness=default_thickness / unit_scale,
|
||||
InnerFilletRadius=default_inner_fillet_radius / unit_scale,
|
||||
OuterFilletRadius=default_outer_fillet_radius / unit_scale,
|
||||
)
|
||||
|
||||
elif representation_template == "FLOW_SEGMENT_CIRCULAR":
|
||||
default_diameter = 0.1
|
||||
profile_name = f"{props.ifc_class}-{default_diameter*1000}"
|
||||
@@ -717,6 +737,21 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
Radius=(default_diameter / 2) / unit_scale,
|
||||
WallThickness=default_thickness,
|
||||
)
|
||||
elif representation_template == "FLOW_SEGMENT_U_SHAPE":
|
||||
default_depth = 0.4
|
||||
default_flange_width = 0.2
|
||||
default_web_thickness = 0.005
|
||||
default_flange_thickness = 0.005
|
||||
profile_name = f"{props.ifc_class}-{default_depth*1000}x{default_flange_width*1000}x{default_web_thickness*1000}x{default_flange_thickness*1000}"
|
||||
profile = tool.Ifc.get().create_entity(
|
||||
"IfcUShapeProfileDef",
|
||||
ProfileName=profile_name,
|
||||
ProfileType="AREA",
|
||||
Depth=default_depth / unit_scale,
|
||||
FlangeWidth=default_flange_width / unit_scale,
|
||||
WebThickness=default_web_thickness / unit_scale,
|
||||
FlangeThickness=default_flange_thickness / unit_scale,
|
||||
)
|
||||
|
||||
rel = ifcopenshell.api.material.assign_material(
|
||||
tool.Ifc.get(), products=[element], type="IfcMaterialProfileSet"
|
||||
|
||||
@@ -45,8 +45,12 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.prop import StrProperty
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import BIMFacet
|
||||
|
||||
def draw_text_editor_header(self, context):
|
||||
|
||||
def draw_text_editor_header(self: bpy.types.TEXT_HT_header, context: bpy.types.Context) -> None:
|
||||
assert isinstance(context.space_data, bpy.types.SpaceTextEditor)
|
||||
if context.space_data.text and context.space_data.text.name.startswith("FilterQuery_"):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
@@ -160,6 +164,7 @@ class FilterValueSuggestions(Operator):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
assert layout
|
||||
|
||||
filter_groups = tool.Search.get_filter_groups(self.module)
|
||||
ifc_filter = filter_groups[self.group_index].filters[self.filter_index]
|
||||
@@ -202,8 +207,8 @@ class FilterValueSuggestions(Operator):
|
||||
results_are_suggestions=True,
|
||||
)
|
||||
|
||||
def get_suggestions(self, ifc_file, ifc_filter):
|
||||
suggestions = set()
|
||||
def get_suggestions(self, ifc_file: ifcopenshell.file, ifc_filter: "BIMFacet") -> set[str]:
|
||||
suggestions: set[str] = set()
|
||||
|
||||
if ifc_filter.type == "entity":
|
||||
suggestions = self.get_entity_suggestions(ifc_file)
|
||||
@@ -236,8 +241,8 @@ class FilterValueSuggestions(Operator):
|
||||
|
||||
return suggestions
|
||||
|
||||
def build_hierarchy_path(self, element):
|
||||
path = []
|
||||
def build_hierarchy_path(self, element: ifcopenshell.entity_instance) -> list[str]:
|
||||
path: list[str] = []
|
||||
current = element
|
||||
|
||||
while current:
|
||||
@@ -262,8 +267,8 @@ class FilterValueSuggestions(Operator):
|
||||
|
||||
return path
|
||||
|
||||
def get_entity_suggestions(self, ifc_file):
|
||||
all_classes = set()
|
||||
def get_entity_suggestions(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
all_classes: set[str] = set()
|
||||
schema = tool.Ifc.schema()
|
||||
|
||||
for element_id in IfcStore.id_map.keys():
|
||||
@@ -273,11 +278,13 @@ class FilterValueSuggestions(Operator):
|
||||
|
||||
try:
|
||||
entity = schema.declaration_by_name(class_name).as_entity()
|
||||
assert entity
|
||||
current = entity
|
||||
|
||||
chain_names = [class_name]
|
||||
while current.supertype():
|
||||
supertype = current.supertype()
|
||||
assert supertype
|
||||
chain_names.insert(0, supertype.name())
|
||||
current = supertype
|
||||
|
||||
@@ -295,8 +302,8 @@ class FilterValueSuggestions(Operator):
|
||||
|
||||
return all_classes
|
||||
|
||||
def get_type_suggestions(self, ifc_file):
|
||||
suggestions = set()
|
||||
def get_type_suggestions(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
suggestions: set[str] = set()
|
||||
for element_type in ifc_file.by_type("IfcTypeObject"):
|
||||
if element_type.Name:
|
||||
hierarchy_path = self.build_hierarchy_path(element_type)
|
||||
@@ -306,15 +313,15 @@ class FilterValueSuggestions(Operator):
|
||||
suggestions.add(element_type.Name)
|
||||
return suggestions
|
||||
|
||||
def get_material_suggestions(self, ifc_file):
|
||||
def get_material_suggestions(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
suggestions = set()
|
||||
for material in ifc_file.by_type("IfcMaterial"):
|
||||
if material.Name:
|
||||
suggestions.add(material.Name)
|
||||
return suggestions
|
||||
|
||||
def get_location_suggestions(self, ifc_file):
|
||||
suggestions = set()
|
||||
def get_location_suggestions(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
suggestions: set[str] = set()
|
||||
for spatial in ifc_file.by_type("IfcSpatialStructureElement"):
|
||||
if spatial.Name:
|
||||
hierarchy_path = self.build_hierarchy_path(spatial)
|
||||
@@ -324,8 +331,8 @@ class FilterValueSuggestions(Operator):
|
||||
suggestions.add(spatial.Name)
|
||||
return suggestions
|
||||
|
||||
def get_group_suggestions(self, ifc_file):
|
||||
suggestions = set()
|
||||
def get_group_suggestions(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
suggestions: set[str] = set()
|
||||
for group in ifc_file.by_type("IfcGroup"):
|
||||
if group.Name:
|
||||
hierarchy_path = self.build_hierarchy_path(group)
|
||||
@@ -335,15 +342,15 @@ class FilterValueSuggestions(Operator):
|
||||
suggestions.add(group.Name)
|
||||
return suggestions
|
||||
|
||||
def get_classification_suggestions(self, ifc_file):
|
||||
suggestions = set()
|
||||
def get_classification_suggestions(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
suggestions: set[str] = set()
|
||||
for ref in ifc_file.by_type("IfcClassificationReference"):
|
||||
if ref.Identification:
|
||||
suggestions.add(ref.Identification)
|
||||
return suggestions
|
||||
|
||||
def get_parent_suggestions(self, ifc_file):
|
||||
suggestions = set()
|
||||
def get_parent_suggestions(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
suggestions: set[str] = set()
|
||||
for element_id in IfcStore.id_map.keys():
|
||||
try:
|
||||
element = ifc_file.by_id(element_id)
|
||||
@@ -371,9 +378,9 @@ class FilterValueSuggestions(Operator):
|
||||
|
||||
return suggestions
|
||||
|
||||
def get_instance_suggestions(self, ifc_file):
|
||||
suggestions = set()
|
||||
element_data = []
|
||||
def get_instance_suggestions(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
suggestions: set[str] = set()
|
||||
element_data: list[tuple[str, str]] = []
|
||||
|
||||
for element_id in IfcStore.id_map.keys():
|
||||
try:
|
||||
@@ -392,7 +399,7 @@ class FilterValueSuggestions(Operator):
|
||||
except:
|
||||
continue
|
||||
|
||||
display_counts = {}
|
||||
display_counts: dict[str, int] = {}
|
||||
for display_str, global_id in element_data:
|
||||
display_counts[display_str] = display_counts.get(display_str, 0) + 1
|
||||
|
||||
@@ -404,8 +411,8 @@ class FilterValueSuggestions(Operator):
|
||||
|
||||
return suggestions
|
||||
|
||||
def get_property_sets(self, ifc_file):
|
||||
psets = set()
|
||||
def get_property_sets(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
psets: set[str] = set()
|
||||
for element_id in IfcStore.id_map.keys():
|
||||
try:
|
||||
element = ifc_file.by_id(element_id)
|
||||
@@ -418,8 +425,8 @@ class FilterValueSuggestions(Operator):
|
||||
continue
|
||||
return psets
|
||||
|
||||
def get_property_names(self, ifc_file, pset_name):
|
||||
property_names = set()
|
||||
def get_property_names(self, ifc_file: ifcopenshell.file, pset_name: str) -> set[str]:
|
||||
property_names: set[str] = set()
|
||||
for element_id in IfcStore.id_map.keys():
|
||||
try:
|
||||
element = ifc_file.by_id(element_id)
|
||||
@@ -435,8 +442,8 @@ class FilterValueSuggestions(Operator):
|
||||
continue
|
||||
return property_names
|
||||
|
||||
def get_property_values(self, ifc_file, pset_name, property_name):
|
||||
property_values = set()
|
||||
def get_property_values(self, ifc_file: ifcopenshell.file, pset_name: str, property_name: str) -> set[str]:
|
||||
property_values: set[str] = set()
|
||||
for element_id in IfcStore.id_map.keys():
|
||||
try:
|
||||
element = ifc_file.by_id(element_id)
|
||||
@@ -462,8 +469,8 @@ class FilterValueSuggestions(Operator):
|
||||
continue
|
||||
return property_values
|
||||
|
||||
def get_attribute_names(self, ifc_file):
|
||||
attribute_names = set()
|
||||
def get_attribute_names(self, ifc_file: ifcopenshell.file) -> set[str]:
|
||||
attribute_names: set[str] = set()
|
||||
schema = tool.Ifc.schema()
|
||||
|
||||
ifc_classes = set()
|
||||
@@ -477,6 +484,7 @@ class FilterValueSuggestions(Operator):
|
||||
for ifc_class in ifc_classes:
|
||||
try:
|
||||
entity = schema.declaration_by_name(ifc_class).as_entity()
|
||||
assert entity
|
||||
attributes = entity.all_attributes()
|
||||
for attr in attributes:
|
||||
attribute_names.add(attr.name())
|
||||
@@ -485,8 +493,8 @@ class FilterValueSuggestions(Operator):
|
||||
|
||||
return attribute_names
|
||||
|
||||
def get_attribute_values(self, ifc_file, attribute_name):
|
||||
attribute_values = set()
|
||||
def get_attribute_values(self, ifc_file: ifcopenshell.file, attribute_name: str) -> set[str]:
|
||||
attribute_values: set[str] = set()
|
||||
|
||||
for element_id in IfcStore.id_map.keys():
|
||||
try:
|
||||
@@ -780,11 +788,10 @@ class Search(Operator):
|
||||
)
|
||||
|
||||
objs = [obj for e in results if isinstance(obj := tool.Ifc.get_object(e), bpy.types.Object)]
|
||||
for obj in objs:
|
||||
tool.Blender.set_object_selection(obj)
|
||||
if objs:
|
||||
tool.Blender.set_active_object(objs[0])
|
||||
self.report({"INFO"}, f"{len(results)} Results.")
|
||||
active_object = next(iter(objs), None)
|
||||
selection = tool.Blender.validate_object_selection(context, active_object, objs)
|
||||
tool.Blender.set_objects_selection(*selection, clear_previous_selection=False)
|
||||
self.report({"INFO"}, f"{len(results)} Results, {len(selection.selected_objects)} Objects Selected")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -153,16 +153,10 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"Assign the selected objects to the container selected in Spatial Manager.\n\n"
|
||||
"All elements-parts of an aggregate will be skipped.\n"
|
||||
"To assign a container, they should be unassigned from an aggregate first.\n\n"
|
||||
"This will also move objects to the container collection in the outliner.\n"
|
||||
"ALT + Click to ensure objects are only linked in the container collection"
|
||||
"This will also move objects to the container collection in the outliner."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||
remove_from_other_containers: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.remove_from_other_containers = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
if self.container:
|
||||
@@ -177,31 +171,9 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
return
|
||||
|
||||
objs: list[bpy.types.Object] = []
|
||||
# In IFC element can be either contained of aggregated,
|
||||
# tehrefore we skip aggregated elements here to prevent confusion.
|
||||
# Can't handle it in `poll` since user might just select bunch of elements
|
||||
# and try to assign a container to them
|
||||
# and excluding aggregates because of the `poll` failing might get awkward.
|
||||
skipped_aggregates = 0
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_aggregate(element):
|
||||
skipped_aggregates += 1
|
||||
continue
|
||||
objs.append(obj)
|
||||
|
||||
for element_obj in objs:
|
||||
if self.remove_from_other_containers:
|
||||
for col in element_obj.users_collection[:]:
|
||||
col.objects.unlink(element_obj)
|
||||
core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj)
|
||||
|
||||
aggregates_msg = ""
|
||||
if skipped_aggregates:
|
||||
aggregates_msg = f" {skipped_aggregates} aggregated elements skipped."
|
||||
self.report({"INFO"}, f"{len(objs)} elements assigned.{aggregates_msg}")
|
||||
core.assign_container(
|
||||
tool.Ifc, tool.Collector, tool.Spatial, container=container, objs=tool.Blender.get_selected_objects()
|
||||
)
|
||||
|
||||
|
||||
class EnableEditingContainer(bpy.types.Operator):
|
||||
|
||||
@@ -99,8 +99,7 @@ def update_name(self: "BIMContainer", context: bpy.types.Context) -> None:
|
||||
tool.Spatial.edit_container_name(element, self.name)
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
tool.Root.set_object_name(obj, element)
|
||||
if collection := tool.Blender.get_object_bim_props(obj).collection:
|
||||
collection.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
|
||||
tool.Collector.assign(obj)
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
|
||||
@@ -175,8 +174,8 @@ def poll_container_obj(self: "BIMObjectSpatialProperties", container_obj: bpy.ty
|
||||
obj = self.id_data
|
||||
if (
|
||||
(container := tool.Ifc.get_entity(container_obj))
|
||||
and (tool.Ifc.get_entity(obj))
|
||||
and tool.Spatial.can_contain(container, obj)
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and tool.Spatial.can_contain(container, element)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -37,6 +37,7 @@ classes = (
|
||||
operator.EditZone,
|
||||
operator.EnableEditingSystem,
|
||||
operator.EnableEditingZone,
|
||||
operator.EstablishPathDirection,
|
||||
operator.HidePorts,
|
||||
operator.LoadSystems,
|
||||
operator.LoadZones,
|
||||
|
||||
@@ -317,17 +317,19 @@ class MEPConnectElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Connect MEP Elements"
|
||||
bl_description = "Connects two selected elements by their closest located ports and adjusts them"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not len(context.selected_objects) == 2:
|
||||
cls.poll_message_set("Need to select 2 objects.")
|
||||
return False
|
||||
return True
|
||||
obj1_name: bpy.props.StringProperty(name="Object 1")
|
||||
obj2_name: bpy.props.StringProperty(name="Object 2")
|
||||
|
||||
def _execute(self, context):
|
||||
obj1 = context.active_object
|
||||
obj2 = next(o for o in context.selected_objects if o != obj1)
|
||||
if self.obj1_name and self.obj2_name:
|
||||
obj1 = bpy.data.objects.get(self.obj1_name)
|
||||
obj2 = bpy.data.objects.get(self.obj2_name)
|
||||
else:
|
||||
if not context.selected_objects or len(context.selected_objects) != 2:
|
||||
self.report({"ERROR"}, "Need to select 2 objects.")
|
||||
return {"CANCELLED"}
|
||||
obj1 = context.active_object
|
||||
obj2 = next(o for o in context.selected_objects if o != obj1)
|
||||
|
||||
tool.Model.sync_object_ifc_position(obj1)
|
||||
tool.Model.sync_object_ifc_position(obj2)
|
||||
@@ -477,6 +479,55 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EstablishPathDirection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.establish_path_direction"
|
||||
bl_label = "Establish Path Direction"
|
||||
bl_description = "Propagates flow direction through connected flow segments with two ports"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
port_id: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
connected_port = tool.Ifc.get().by_id(self.port_id)
|
||||
|
||||
if not connected_port or not connected_port.is_a("IfcDistributionPort"):
|
||||
self.report({"ERROR"}, "Invalid port specified.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
direction_map = {
|
||||
"SOURCE": "SINK",
|
||||
"SINK": "SOURCE",
|
||||
"SOURCEANDSINK": "SOURCEANDSINK",
|
||||
"NOTDEFINED": "NOTDEFINED",
|
||||
}
|
||||
next_element = tool.System.get_port_relating_element(connected_port)
|
||||
ports = tool.System.get_ports(next_element)
|
||||
segments_processed = 0
|
||||
while len(ports) == 2:
|
||||
if ports[0].id() == connected_port.id():
|
||||
other_port = ports[1]
|
||||
else:
|
||||
other_port = ports[0]
|
||||
|
||||
new_direction = direction_map.get(connected_port.FlowDirection, "NOTDEFINED")
|
||||
other_port.FlowDirection = new_direction
|
||||
segments_processed += 1
|
||||
|
||||
connected_port = tool.System.get_connected_port(other_port)
|
||||
if not connected_port:
|
||||
break
|
||||
connected_port.FlowDirection = direction_map.get(other_port.FlowDirection, "NOTDEFINED")
|
||||
next_element = tool.System.get_port_relating_element(connected_port)
|
||||
|
||||
if not next_element.is_a("IfcFlowSegment"):
|
||||
print(f"DEBUG: next_element is not IfcFlowSegment, stopping")
|
||||
break
|
||||
|
||||
ports = tool.System.get_ports(next_element)
|
||||
|
||||
self.report({"INFO"}, f"Established path direction through {segments_processed} flow segment(s).")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadZones(bpy.types.Operator):
|
||||
bl_idname = "bim.load_zones"
|
||||
bl_label = "Load Zones"
|
||||
|
||||
@@ -296,7 +296,7 @@ class BIM_PT_port(Panel):
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
|
||||
row = layout.row(align=True)
|
||||
cols = [row.column(align=True) for i in range(9)]
|
||||
cols = [row.column(align=True) for i in range(10)]
|
||||
cols[3].scale_x = 1.0
|
||||
cols[6].scale_x = 1.0
|
||||
cols[8].scale_x = 1.33
|
||||
@@ -333,12 +333,16 @@ class BIM_PT_port(Panel):
|
||||
else:
|
||||
cols[7].label(text="", icon="BLANK1")
|
||||
cols[8].label(text="")
|
||||
cols[9].operator("bim.establish_path_direction", text="", icon="CON_FOLLOWPATH").port_id = (
|
||||
connected_port.id()
|
||||
)
|
||||
else:
|
||||
cols[4].label(text="", icon="BLANK1")
|
||||
cols[5].label(text="", icon="BLANK1")
|
||||
cols[6].label(text="Port is disconnected")
|
||||
cols[7].label(text="", icon="BLANK1")
|
||||
cols[8].label(text="")
|
||||
cols[9].label(text="", icon="BLANK1")
|
||||
|
||||
|
||||
class BIM_PT_flow_controls(Panel):
|
||||
|
||||
@@ -721,13 +721,20 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
|
||||
chain_filter_with_set_operations: BoolProperty(
|
||||
name="NEW Filter mode: Enable chained filters with set operations",
|
||||
description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values",
|
||||
description=(
|
||||
"Enable chaining search filters with set operations: "
|
||||
"ADD (union: combine sets), SUBTRACT (difference: remove from set), "
|
||||
"FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values"
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
|
||||
save_metadata_blend_file: BoolProperty(
|
||||
name="Save non ifc data to metadata blend File",
|
||||
description="Save session data (window layout, settings) to a metadata blend file alongside the IFC file. This file is automatically loaded when opening the project.",
|
||||
description=(
|
||||
"Save session data (window layout, settings) to a metadata blend file alongside the IFC file. "
|
||||
"This file is automatically loaded when opening the project."
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
metadata_blend_file_suffix: StringProperty(
|
||||
|
||||
@@ -39,12 +39,44 @@ def disable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) ->
|
||||
|
||||
|
||||
def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
|
||||
drawing.synchronise_ifc_and_text_attributes(obj)
|
||||
drawing.update_text_size_pset(obj)
|
||||
drawing.update_text_annotation_properties(obj)
|
||||
literal_attributes = drawing.export_text_literal_attributes(obj)
|
||||
drawing.edit_text_font_size(obj, drawing.export_font_size(obj))
|
||||
drawing.edit_text_wrap_length(obj, drawing.export_wrap_length(obj))
|
||||
drawing.edit_text_symbol(obj, drawing.export_symbol(obj))
|
||||
drawing.edit_text_literals(obj, literal_attributes)
|
||||
drawing.disable_editing_text(obj)
|
||||
|
||||
|
||||
def copy_text_to_selection(
|
||||
drawing: type[tool.Drawing],
|
||||
attribute: Literal["FONT_SIZE", "ALIGNMENT", "WRAP_LENGTH", "SYMBOL", "LITERALS"],
|
||||
attribute_obj: bpy.types.Object,
|
||||
apply_objs: list[bpy.types.Object],
|
||||
) -> None:
|
||||
if attribute == "FONT_SIZE":
|
||||
data = drawing.export_font_size(attribute_obj)
|
||||
elif attribute == "ALIGNMENT":
|
||||
data = drawing.export_alignment(attribute_obj)
|
||||
elif attribute == "WRAP_LENGTH":
|
||||
data = drawing.export_wrap_length(attribute_obj)
|
||||
elif attribute == "SYMBOL":
|
||||
data = drawing.export_symbol(attribute_obj)
|
||||
elif attribute == "LITERALS":
|
||||
data = drawing.export_text_literal_attributes(attribute_obj)
|
||||
for obj in apply_objs:
|
||||
if attribute == "FONT_SIZE":
|
||||
drawing.edit_text_font_size(obj, data)
|
||||
elif attribute == "ALIGNMENT":
|
||||
drawing.edit_text_alignment(obj, data)
|
||||
elif attribute == "WRAP_LENGTH":
|
||||
drawing.edit_text_wrap_length(obj, data)
|
||||
elif attribute == "SYMBOL":
|
||||
drawing.edit_text_symbol(obj, data)
|
||||
elif attribute == "LITERALS":
|
||||
drawing.edit_text_literals(obj, data)
|
||||
drawing.disable_editing_text(obj)
|
||||
|
||||
|
||||
def enable_editing_assigned_product(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
|
||||
drawing.enable_editing_assigned_product(obj)
|
||||
drawing.import_assigned_product(obj)
|
||||
|
||||
@@ -52,15 +52,22 @@ def assign_container(
|
||||
collector: type[tool.Collector],
|
||||
spatial: type[tool.Spatial],
|
||||
container: ifcopenshell.entity_instance,
|
||||
element_obj: Optional[bpy.types.Object] = None,
|
||||
objs: Optional[bpy.types.Object] = None,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if not spatial.can_contain(container, element_obj):
|
||||
return
|
||||
assert element_obj # Type checker.
|
||||
rel = ifc.run("spatial.assign_container", products=[ifc.get_entity(element_obj)], relating_structure=container)
|
||||
spatial.disable_editing(element_obj)
|
||||
collector.assign(element_obj)
|
||||
return rel
|
||||
root_elements = set()
|
||||
all_elements = set()
|
||||
for obj in objs:
|
||||
if not (element := ifc.get_entity(obj)):
|
||||
continue
|
||||
root_element = spatial.get_root_element(element)
|
||||
root_elements.add(root_element)
|
||||
spatial.disable_editing(obj)
|
||||
all_elements.add(root_element)
|
||||
all_elements.update(spatial.get_decomposition(root_element))
|
||||
if products := [e for e in root_elements if spatial.can_contain(container, root_element)]:
|
||||
ifc.run("spatial.assign_container", products=products, relating_structure=container)
|
||||
for element in all_elements:
|
||||
collector.assign(ifc.get_object(element))
|
||||
|
||||
|
||||
def enable_editing_container(spatial: type[tool.Spatial], obj: bpy.types.Object) -> None:
|
||||
@@ -98,7 +105,7 @@ def copy_to_container(
|
||||
copied_obj = spatial.duplicate_object_and_data(obj)
|
||||
spatial.set_relative_object_matrix(copied_obj, to_container_obj, matrix)
|
||||
result_objs.append(spatial.run_root_copy_class(obj=copied_obj))
|
||||
spatial.run_spatial_assign_container(container=to_container, element_obj=copied_obj)
|
||||
spatial.run_spatial_assign_container(container=to_container, objs=[copied_obj])
|
||||
spatial.disable_editing(obj)
|
||||
return result_objs
|
||||
|
||||
|
||||
@@ -334,6 +334,11 @@ class Drawing:
|
||||
def disable_editing_sheets(cls): pass
|
||||
def disable_editing_text(cls, obj): pass
|
||||
def does_file_exist(cls, uri): pass
|
||||
def edit_text_alignment(cls, obj, alignment): pass
|
||||
def edit_text_font_size(cls, obj, size): pass
|
||||
def edit_text_literals(cls, obj, literals): pass
|
||||
def edit_text_symbol(cls, obj, symbol): pass
|
||||
def edit_text_wrap_length(cls, obj, wrap_length): pass
|
||||
def enable_editing(cls, obj): pass
|
||||
def enable_editing_assigned_product(cls, obj): pass
|
||||
def enable_editing_drawings(cls): pass
|
||||
@@ -402,10 +407,7 @@ class Drawing:
|
||||
def setup_shading_styles_path(cls, resource_path): pass
|
||||
def show_decorations(cls): pass
|
||||
def sync_object_placement(cls, obj): pass
|
||||
def synchronise_ifc_and_text_attributes(cls, obj): pass
|
||||
def update_embedded_svg_location(cls, uri, old_location, new_location): pass
|
||||
def update_text_annotation_properties(cls, obj): pass
|
||||
def update_text_size_pset(cls, obj): pass
|
||||
|
||||
|
||||
@interface
|
||||
@@ -951,15 +953,17 @@ class Spatial:
|
||||
def get_active_container(cls): pass
|
||||
def get_container(cls, element): pass
|
||||
def get_decomposed_elements(cls, container, recursive): pass
|
||||
def get_decomposition(cls, element): pass
|
||||
def get_object_matrix(cls, obj): pass
|
||||
def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass
|
||||
def get_root_element(cls, element): pass
|
||||
def get_selected_product_types(cls): pass
|
||||
def get_selected_products(cls): pass
|
||||
def import_spatial_decomposition(cls): pass
|
||||
def import_spatial_element(cls, element, level_index): pass
|
||||
def load_contained_elements(cls): pass
|
||||
def run_root_copy_class(cls, obj): pass
|
||||
def run_spatial_assign_container(cls, container, element_obj): pass
|
||||
def run_spatial_assign_container(cls, container, objs): pass
|
||||
def run_spatial_import_spatial_decomposition(cls): pass
|
||||
def select_object(cls, obj): pass
|
||||
def select_products(cls, products, unhide=False): pass
|
||||
@@ -1123,15 +1127,15 @@ class Unit:
|
||||
def disable_editing_units(cls): pass
|
||||
def enable_editing_units(cls): pass
|
||||
def export_unit_attributes(cls): pass
|
||||
def get_currency_name(cls): pass
|
||||
def get_project_currency_unit(cls): pass
|
||||
def get_scene_unit_name(cls, unit_type): pass
|
||||
def get_scene_unit_si_prefix(cls, name): pass
|
||||
def import_unit_attributes(cls, unit): pass
|
||||
def import_units(cls): pass
|
||||
def is_scene_unit_metric(cls): pass
|
||||
def is_si_unit(cls, name): pass
|
||||
def is_unit_class(cls, unit, ifc_class): pass
|
||||
def set_active_unit(cls, unit): pass
|
||||
def get_project_currency_unit(cls): pass
|
||||
def get_currency_name(cls): pass
|
||||
|
||||
@interface
|
||||
class Voider:
|
||||
|
||||
@@ -36,6 +36,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
@@ -49,7 +50,7 @@ import ifcopenshell.util.element
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from ifcopenshell import entity_instance
|
||||
from mathutils import Vector
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.bim
|
||||
import bonsai.core.tool
|
||||
@@ -718,13 +719,18 @@ class Blender(bonsai.core.tool.Blender):
|
||||
if active_object:
|
||||
active_object.select_set(True)
|
||||
|
||||
class ObjectsSelectionArgs(NamedTuple):
|
||||
context: bpy.types.Context
|
||||
active_object: bpy.types.Object | None
|
||||
selected_objects: list[bpy.types.Object]
|
||||
|
||||
@classmethod
|
||||
def validate_object_selection(
|
||||
cls,
|
||||
context: bpy.types.Context,
|
||||
active_object: Union[bpy.types.Object, None] = None,
|
||||
selected_objects: Sequence[bpy.types.Object] = (),
|
||||
) -> tuple[bpy.types.Context, Union[bpy.types.Object, None], list[bpy.types.Object]]:
|
||||
) -> ObjectsSelectionArgs:
|
||||
"""Validate object selection and return only valid objects.
|
||||
|
||||
Can be used before ``set_objects_selection`` to avoid errors
|
||||
@@ -734,12 +740,15 @@ class Blender(bonsai.core.tool.Blender):
|
||||
assert context.view_layer
|
||||
view_layer_objects = set(context.view_layer.objects)
|
||||
|
||||
new_selected_objects = [o for o in selected_objects if cls.is_valid_data_block(o) and o in view_layer_objects]
|
||||
def is_selectable(obj: bpy.types.Object) -> bool:
|
||||
return cls.is_valid_data_block(obj) and obj in view_layer_objects
|
||||
|
||||
if active_object and not cls.is_valid_data_block(active_object):
|
||||
new_selected_objects = [o for o in selected_objects if is_selectable(o)]
|
||||
|
||||
if active_object and not is_selectable(active_object):
|
||||
active_object = None
|
||||
|
||||
return context, active_object, new_selected_objects
|
||||
return cls.ObjectsSelectionArgs(context, active_object, new_selected_objects)
|
||||
|
||||
@classmethod
|
||||
def clear_objects_selection(cls) -> None:
|
||||
@@ -2127,3 +2136,32 @@ class Blender(bonsai.core.tool.Blender):
|
||||
if cls.BLENDER_5:
|
||||
return "BLENDER_EEVEE"
|
||||
return "BLENDER_EEVEE_NEXT"
|
||||
|
||||
@classmethod
|
||||
def np_frombuffer_legacy(cls, bytedata: bytes, n: int) -> npt.NDArray[np.float32]:
|
||||
"""
|
||||
Read ``n`` float values from ``bytedata``, regardless if they are stored as ``float32`` or ``float64``.
|
||||
Needed to support .blend files saved in Blender <5.0.0.
|
||||
Also allows to work with .blend files from 5.0.0+ in older Blender versions.
|
||||
|
||||
In ``bpy.app.version >= 5.0.0`` ``mathutils`` transitioned to use ``float32`` buffer type,
|
||||
while in previous version they were using ``float64``.
|
||||
In some cases we are storing raw bytes (e.g. object transforms cheksums), so old .blend files
|
||||
might still have ``float64`` data stored.
|
||||
|
||||
See https://projects.blender.org/blender/blender/issues/149283
|
||||
"""
|
||||
if len(bytedata) == (n * 2):
|
||||
return np.frombuffer(bytedata, dtype=np.float64).astype(np.float32)
|
||||
return np.frombuffer(bytedata, dtype=np.float32)
|
||||
|
||||
@classmethod
|
||||
def np_array_legacy(cls, mathutils_type: Union[Vector, Matrix]) -> npt.NDArray[np.float32]:
|
||||
"""
|
||||
Converts ``mathutils`` types to ``np.float32`` arrays, regardless of Blender version.
|
||||
|
||||
See ``np_frombuffer_legacy`` for more details.
|
||||
"""
|
||||
if cls.BLENDER_5:
|
||||
return np.array(mathutils_type)
|
||||
return np.array(mathutils_type, dtype=np.float32)
|
||||
|
||||
@@ -46,6 +46,8 @@ class Collector(bonsai.core.tool.Collector):
|
||||
# Note that tool.Geometry.is_locked is only checked within the if
|
||||
# statements for efficiency as it is a slow check.
|
||||
tool.Geometry.lock_scale(obj)
|
||||
if element.is_a("IfcSlab"):
|
||||
tool.Geometry.lock_rotation(obj, x=True)
|
||||
|
||||
if element.is_a("IfcGridAxis"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
|
||||
@@ -23,6 +23,7 @@ from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, assert_never
|
||||
|
||||
import aud
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.cost
|
||||
@@ -143,25 +144,18 @@ class Cost(bonsai.core.tool.Cost):
|
||||
|
||||
@classmethod
|
||||
def play_sound(cls) -> None:
|
||||
if tool.Blender.get_addon_preferences().should_play_chaching_sound:
|
||||
# Save ears for those running the tests in background mode.
|
||||
if tool.Blender.get_addon_preferences().should_play_chaching_sound and not bpy.app.background:
|
||||
cls.play_chaching_sound() # lol
|
||||
|
||||
@classmethod
|
||||
def play_chaching_sound(cls) -> None:
|
||||
# TODO: make pitch higher as costs rise
|
||||
try:
|
||||
import aud
|
||||
|
||||
device = aud.Device()
|
||||
# chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/
|
||||
sound = aud.Sound(tool.Blender.get_data_dir_path(filename="chaching.mp3").__str__())
|
||||
handle = device.play(sound)
|
||||
sound_buffered = aud.Sound.buffer(sound)
|
||||
handle_buffered = device.play(sound_buffered)
|
||||
handle.stop()
|
||||
handle_buffered.stop()
|
||||
except:
|
||||
pass # ah well
|
||||
device = aud.Device()
|
||||
# chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/
|
||||
filepath = tool.Blender.get_data_dir_path("chaching.mp3").__str__()
|
||||
sound = aud.Sound(filepath)
|
||||
device.play(sound)
|
||||
|
||||
@classmethod
|
||||
def load_cost_schedule_tree(cls) -> None:
|
||||
|
||||
@@ -608,6 +608,25 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
literals.append(literal_data)
|
||||
return literals
|
||||
|
||||
@classmethod
|
||||
def export_font_size(cls, obj: bpy.types.Object) -> str:
|
||||
return float(cls.get_text_props(obj).font_size)
|
||||
|
||||
@classmethod
|
||||
def export_alignment(cls, obj: bpy.types.Object) -> str:
|
||||
props = cls.get_text_props(obj)
|
||||
if (alignment := props.align_vertical + "-" + props.align_horizontal) == "middle-middle":
|
||||
return "center"
|
||||
return alignment
|
||||
|
||||
@classmethod
|
||||
def export_wrap_length(cls, obj: bpy.types.Object) -> str:
|
||||
return cls.get_text_props(obj).newline_at
|
||||
|
||||
@classmethod
|
||||
def export_symbol(cls, obj: bpy.types.Object) -> str:
|
||||
return cls.get_text_props(obj).get_symbol()
|
||||
|
||||
@classmethod
|
||||
def create_annotation_context(
|
||||
cls, target_view: str, object_type: Optional[str] = None
|
||||
@@ -835,43 +854,12 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
return props.is_editing_sheets
|
||||
|
||||
@classmethod
|
||||
def synchronise_ifc_and_text_attributes(cls, obj: bpy.types.Object) -> None:
|
||||
def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None:
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
assert (rep := cls.get_annotation_representation(element))
|
||||
|
||||
old_literals = cls.get_text_literal(obj, return_list=True)
|
||||
assert isinstance(old_literals, list)
|
||||
literals_attributes = cls.export_text_literal_attributes(obj)
|
||||
props = cls.get_text_props(obj)
|
||||
defined_ifc_ids = [l.ifc_definition_id for l in props.literals]
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
added_literals: list[ifcopenshell.entity_instance] = []
|
||||
new_literals: list[ifcopenshell.entity_instance] = []
|
||||
for ifc_definition_id, attributes in zip(defined_ifc_ids, literals_attributes):
|
||||
# making sure all literals from text edit exist in ifc
|
||||
if ifc_definition_id == 0:
|
||||
literal = cls.add_literal(**attributes)
|
||||
added_literals.append(literal)
|
||||
else:
|
||||
literal = ifc_file.by_id(ifc_definition_id)
|
||||
ifcopenshell.api.drawing.edit_text_literal(
|
||||
ifc_file,
|
||||
text_literal=literal,
|
||||
attributes=attributes,
|
||||
)
|
||||
new_literals.append(literal)
|
||||
|
||||
removed_literals = set(old_literals) - set(new_literals)
|
||||
|
||||
# Add new literals and keep the order as defined in text props.
|
||||
items = [i for i in rep.Items if i not in removed_literals] + added_literals
|
||||
items.sort(key=lambda x: new_literals.index(x) if x in new_literals else -1)
|
||||
rep.Items = items
|
||||
|
||||
# Remove from ifc the literals that were removed during the edit.
|
||||
for literal in removed_literals:
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, literal)
|
||||
for literal in cls.get_text_literal(obj, return_list=True):
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), literal)
|
||||
rep.Items = [cls.add_literal(**a) for a in literal_attributes]
|
||||
|
||||
@classmethod
|
||||
def add_literal(cls, **attributes: str) -> ifcopenshell.entity_instance:
|
||||
@@ -1212,8 +1200,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
props.font_size = str(text_data["FontSize"])
|
||||
props.newline_at = text_data["Newline_At"]
|
||||
props.set_symbol(text_data["Symbol"])
|
||||
props.reverse_list = text_data["Reverse_List"]
|
||||
props.list_separator = text_data["List_Separator"]
|
||||
|
||||
@classmethod
|
||||
def import_assigned_product(cls, obj: bpy.types.Object) -> None:
|
||||
@@ -1310,7 +1296,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
props.should_draw_decorations = True
|
||||
|
||||
@classmethod
|
||||
def update_text_size_pset(cls, obj: bpy.types.Object) -> None:
|
||||
def edit_text_font_size(cls, obj: bpy.types.Object, font_size: float) -> None:
|
||||
"""updates pset `EPset_Annotation.Classes` value
|
||||
based on current font size from `obj.BIMTextProperties.font_size`
|
||||
"""
|
||||
@@ -1320,8 +1306,9 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
# updating text font size in EPset_Annotation.Classes
|
||||
font_size = float(props.font_size)
|
||||
print("we got", font_size, repr(font_size))
|
||||
font_size_str = next((key for key in FONT_SIZES if FONT_SIZES[key] == font_size), None)
|
||||
print("so", font_size_str)
|
||||
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
||||
assert isinstance(classes, Union[str, None])
|
||||
classes_split = classes.split() if classes else []
|
||||
@@ -1341,34 +1328,32 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
pset = tool.Pset.get_element_pset(element, "EPset_Annotation")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation")
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties={"Classes": classes},
|
||||
)
|
||||
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"Classes": classes})
|
||||
|
||||
@classmethod
|
||||
def update_text_annotation_properties(cls, obj: bpy.types.Object) -> None:
|
||||
"""Update all EPset_Annotation properties from the text props"""
|
||||
props = cls.get_text_props(obj)
|
||||
def edit_text_wrap_length(cls, obj: bpy.types.Object, wrap_length: int) -> None:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = tool.Pset.get_element_pset(element, "EPset_Annotation")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation")
|
||||
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"Newline_At": wrap_length})
|
||||
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties={
|
||||
"Newline_At": int(props.newline_at),
|
||||
"Symbol": props.get_symbol(),
|
||||
"Reverse_List": props.reverse_list,
|
||||
"List_Separator": props.list_separator or "",
|
||||
},
|
||||
)
|
||||
@classmethod
|
||||
def edit_text_symbol(cls, obj: bpy.types.Object, symbol: str) -> None:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = tool.Pset.get_element_pset(element, "EPset_Annotation")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation")
|
||||
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"Symbol": symbol})
|
||||
|
||||
@classmethod
|
||||
def edit_text_alignment(cls, obj: bpy.types.Object, alignment: str) -> None:
|
||||
ifc_literals = cls.get_text_literal(obj, return_list=True)
|
||||
for ifc_literal in ifc_literals or []:
|
||||
if ifc_literal.is_a("IfcTextLiteralWithExtent"):
|
||||
ifc_literal.BoxAlignment = alignment
|
||||
|
||||
# TODO below this point is highly experimental prototype code with no tests
|
||||
|
||||
@@ -2112,13 +2097,9 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
cls,
|
||||
text: str,
|
||||
product: Optional[ifcopenshell.entity_instance] = None,
|
||||
reverse_list: bool = False,
|
||||
list_separator: str = ", ",
|
||||
) -> str:
|
||||
if not product:
|
||||
return text
|
||||
if list_separator:
|
||||
list_separator = list_separator.encode().decode("unicode_escape")
|
||||
|
||||
for command in re.findall("``.*?``", text):
|
||||
original_command = command
|
||||
@@ -2133,10 +2114,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
for variable in re.findall("{{.*?}}", text):
|
||||
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
||||
if isinstance(value, (list, tuple)):
|
||||
if reverse_list:
|
||||
value = list_separator.join(str(v) for v in reversed(value))
|
||||
else:
|
||||
value = list_separator.join(str(v) for v in value)
|
||||
value = ", ".join(str(v) for v in value)
|
||||
text = text.replace(variable, str(value))
|
||||
return text
|
||||
|
||||
|
||||
@@ -1152,9 +1152,8 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
def record_object_position(cls, obj: bpy.types.Object) -> None:
|
||||
# These are recorded separately because they have different numerical tolerances
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
# Explicit dtype for Blender <5.0 compatibility.
|
||||
props.location_checksum = repr(np.array(obj.matrix_world.translation, dtype=np.float32).tobytes())
|
||||
props.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
|
||||
props.location_checksum = repr(tool.Blender.np_array_legacy(obj.matrix_world.translation).tobytes())
|
||||
props.rotation_checksum = repr(tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()).tobytes())
|
||||
|
||||
@classmethod
|
||||
def remove_connection(cls, connection: ifcopenshell.entity_instance) -> None:
|
||||
|
||||
@@ -115,24 +115,17 @@ class Ifc(bonsai.core.tool.Ifc):
|
||||
return True # Let's be conservative
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
loc_checksum_bytes = eval(oprops.location_checksum)
|
||||
if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64)
|
||||
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32)
|
||||
else: # New format: 3 * 4 bytes (float32)
|
||||
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32)
|
||||
|
||||
loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten()
|
||||
loc_checksum_bytes: bytes = eval(oprops.location_checksum)
|
||||
loc_check = tool.Blender.np_frombuffer_legacy(loc_checksum_bytes, 3)
|
||||
loc_real = tool.Blender.np_array_legacy(obj.matrix_world.translation)
|
||||
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||
return True
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
rot_checksum_bytes = eval(oprops.rotation_checksum)
|
||||
if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64)
|
||||
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3)
|
||||
else: # New format: 9 * 4 bytes (float32)
|
||||
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3)
|
||||
|
||||
rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32)
|
||||
rot_checksum_bytes: bytes = eval(oprops.rotation_checksum)
|
||||
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9)
|
||||
rot_check = rot_check.reshape(3, 3)
|
||||
rot_real = tool.Blender.np_array_legacy(obj.matrix_world.to_3x3())
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
if angle_rad > 0.0017453292519943296: # 0.1 degrees
|
||||
|
||||
@@ -64,10 +64,13 @@ class IfcGit:
|
||||
|
||||
@classmethod
|
||||
def clone_repo(cls, remote_url: str, local_folder: str) -> git.Repo:
|
||||
IfcGitRepo.repo = git.Repo.clone_from(
|
||||
repo = git.Repo.clone_from(
|
||||
url=remote_url,
|
||||
to_path=local_folder,
|
||||
)
|
||||
# Close the repo to release stale subprocess
|
||||
repo.close()
|
||||
IfcGitRepo.repo = git.Repo(local_folder)
|
||||
cls.config_info_attributes(IfcGitRepo.repo)
|
||||
return IfcGitRepo.repo
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import numpy as np
|
||||
import numpy.typing as npt
|
||||
from ifcopenshell.util.shape_builder import np_to_4d
|
||||
from mathutils import Matrix, Vector
|
||||
from mathutils.kdtree import KDTree
|
||||
|
||||
import bonsai.bim.import_ifc
|
||||
import bonsai.core.tool
|
||||
@@ -562,7 +563,18 @@ class Loader(bonsai.core.tool.Loader):
|
||||
|
||||
bm_verts = np.array([v.co for v in bm.verts])
|
||||
coords_scaled = np.array(faceset.Coordinates.CoordList) * si_conversion
|
||||
coordinates_remap = [np.argmin(np.sum((bm_verts - co) ** 2, axis=1)) for co in coords_scaled]
|
||||
# See #2824. IfcIndexedColourMap is not natively handled by IfcOpenShell
|
||||
# As a result, we map IFC coords to Blender coords (highly wasteful but...)
|
||||
# coordinates_remap = [np.argmin(np.sum((bm_verts - co) ** 2, axis=1)) for co in coords_scaled]
|
||||
# Because this is O(N*M), here is a faster KDTree implementation.
|
||||
kd = KDTree(len(bm_verts))
|
||||
for i, v in enumerate(bm_verts):
|
||||
kd.insert((float(v[0]), float(v[1]), float(v[2])), i)
|
||||
kd.balance()
|
||||
coordinates_remap = np.empty(len(coords_scaled), dtype=np.int32)
|
||||
for j, co in enumerate(coords_scaled):
|
||||
_co, index, _dist = kd.find((float(co[0]), float(co[1]), float(co[2])))
|
||||
coordinates_remap[j] = index
|
||||
|
||||
# ifc indices start with 1
|
||||
remap_verts_to_blender = lambda ifc_verts: [coordinates_remap[i - 1] for i in ifc_verts]
|
||||
@@ -1024,7 +1036,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
elif material.is_a("IfcMaterialLayerSetUsage"):
|
||||
usage = material
|
||||
layer_set = material.ForLayerSet
|
||||
offset = usage.OffsetFromReferenceLine
|
||||
offset = usage.OffsetFromReferenceLine * cls.unit_scale
|
||||
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
usage = None
|
||||
@@ -1033,168 +1045,61 @@ class Loader(bonsai.core.tool.Loader):
|
||||
sense_factor = 1
|
||||
else:
|
||||
return mesh
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
return mesh
|
||||
|
||||
# Get mesh bounds
|
||||
if len(mesh.vertices) > 0:
|
||||
z_coords = [v.co.z for v in mesh.vertices]
|
||||
mesh_z_min = min(z_coords)
|
||||
mesh_z_max = max(z_coords)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
|
||||
prev_co = None
|
||||
advance_direction = None
|
||||
|
||||
if not usage:
|
||||
sense_factor = 1
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
advance_direction = no
|
||||
elif usage.LayerSetDirection == "AXIS2":
|
||||
# Get local extrusion direction
|
||||
local_extrusion = Vector([0.0, 0.0, 1.0])
|
||||
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
local_extrusion = Vector(item.ExtrudedDirection.DirectionRatios).normalized()
|
||||
break
|
||||
|
||||
# Thickness direction: perpendicular to extrusion and length
|
||||
thickness_dir = local_extrusion.cross(Vector([1.0, 0.0, 0.0])).normalized()
|
||||
if thickness_dir.y < 0:
|
||||
thickness_dir = -thickness_dir
|
||||
|
||||
no = thickness_dir
|
||||
|
||||
# Find start point by projecting vertices onto thickness direction
|
||||
if len(mesh.vertices) > 0:
|
||||
projections = [Vector(v.co).dot(no) for v in mesh.vertices]
|
||||
min_proj = min(projections)
|
||||
max_proj = max(projections)
|
||||
|
||||
centroid = sum((Vector(v.co) for v in mesh.vertices), Vector()) / len(mesh.vertices)
|
||||
centroid_proj = centroid.dot(no)
|
||||
|
||||
if sense_factor == 1:
|
||||
start_proj = min_proj
|
||||
else:
|
||||
start_proj = max_proj
|
||||
|
||||
offset_dist = start_proj - centroid_proj
|
||||
co = centroid + no * offset_dist
|
||||
|
||||
actual_mesh_height = max_proj - min_proj
|
||||
else:
|
||||
co = Vector((0.0, 0.0, 0.0))
|
||||
|
||||
advance_direction = thickness_dir
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
# AXIS3 layers go through slab thickness (local Z)
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
|
||||
# Find start point by projecting vertices onto Z direction
|
||||
if len(mesh.vertices) > 0:
|
||||
projections = [Vector(v.co).dot(no) for v in mesh.vertices]
|
||||
min_proj = min(projections)
|
||||
max_proj = max(projections)
|
||||
|
||||
centroid = sum((Vector(v.co) for v in mesh.vertices), Vector()) / len(mesh.vertices)
|
||||
centroid_proj = centroid.dot(no)
|
||||
|
||||
if sense_factor == 1:
|
||||
start_proj = min_proj
|
||||
else:
|
||||
start_proj = max_proj
|
||||
|
||||
offset = start_proj - centroid_proj
|
||||
co = centroid + no * offset
|
||||
|
||||
actual_mesh_height = max_proj - min_proj
|
||||
else:
|
||||
co = Vector((0.0, 0.0, 0.0))
|
||||
|
||||
advance_direction = no
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
advance_direction = no
|
||||
|
||||
# Apply DirectionSense
|
||||
if usage and usage.LayerSetDirection == "AXIS2":
|
||||
if sense_factor == -1:
|
||||
advance_direction = -advance_direction
|
||||
test_normal = -no
|
||||
else:
|
||||
test_normal = no
|
||||
elif usage and usage.LayerSetDirection == "AXIS1":
|
||||
no = no * sense_factor
|
||||
advance_direction = advance_direction * sense_factor
|
||||
test_normal = no
|
||||
elif usage and usage.LayerSetDirection == "AXIS3":
|
||||
if sense_factor == -1:
|
||||
advance_direction = -advance_direction
|
||||
test_normal = -no
|
||||
else:
|
||||
test_normal = no
|
||||
else:
|
||||
test_normal = no
|
||||
|
||||
# Cache material styles
|
||||
no *= sense_factor
|
||||
# Cache this
|
||||
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
styles = {}
|
||||
has_layer_styles = False
|
||||
for i, material in enumerate(mesh.materials):
|
||||
if style := tool.Ifc.get_entity(material):
|
||||
styles[style] = i
|
||||
|
||||
layer_list = list(enumerate(layer_set.MaterialLayers))
|
||||
|
||||
# Calculate scale factor
|
||||
total_layer_thickness = sum(layer.LayerThickness for _, layer in layer_list)
|
||||
|
||||
if "actual_mesh_height" not in locals():
|
||||
actual_mesh_height = mesh_z_max - mesh_z_min if len(mesh.vertices) > 0 else total_layer_thickness
|
||||
|
||||
thickness_scale = actual_mesh_height / total_layer_thickness if total_layer_thickness > 0 else 1.0
|
||||
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
|
||||
for idx, (original_i, layer) in enumerate(layer_list):
|
||||
if idx != last_i:
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
if i != last_i:
|
||||
prev_co = co.copy()
|
||||
advance_vector = advance_direction * layer.LayerThickness * thickness_scale
|
||||
co += advance_vector
|
||||
|
||||
co += no * layer.LayerThickness * cls.unit_scale
|
||||
bisect_geom = bmesh.ops.bisect_plane(
|
||||
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
|
||||
)
|
||||
bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"])
|
||||
|
||||
if not (style := ifcopenshell.util.representation.get_material_style(layer.Material, body)):
|
||||
continue
|
||||
if (material_index := styles.get(style, None)) is None:
|
||||
material_index = len(mesh.materials)
|
||||
mesh.materials.append(tool.Ifc.get_object(style))
|
||||
|
||||
if idx == last_i:
|
||||
if i == last_i:
|
||||
for face in bisect_geom["geom"]:
|
||||
if isinstance(face, bmesh.types.BMFace):
|
||||
center = face.calc_center_median()
|
||||
if (center - co).dot(test_normal) >= 0:
|
||||
if (center - co).dot(no) >= 0:
|
||||
face.material_index = material_index
|
||||
has_layer_styles = True
|
||||
else:
|
||||
for face in bisect_geom["geom"]:
|
||||
if isinstance(face, bmesh.types.BMFace):
|
||||
center = face.calc_center_median()
|
||||
if (center - co).dot(test_normal) < 0 and (center - prev_co).dot(test_normal) >= 0:
|
||||
if (center - co).dot(no) < 0 and (center - prev_co).dot(no) >= 0:
|
||||
face.material_index = material_index
|
||||
has_layer_styles = True
|
||||
|
||||
@@ -1207,35 +1112,13 @@ class Loader(bonsai.core.tool.Loader):
|
||||
return mesh
|
||||
|
||||
@classmethod
|
||||
def get_extrusion_vector(cls, element):
|
||||
"""Get the extrusion direction in WORLD coordinates (accounting for object rotation)"""
|
||||
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
|
||||
def get_extrusion_vector(cls, wall):
|
||||
if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
local_direction = Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
|
||||
# Transform to world coordinates using object rotation
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
# Apply object rotation to get actual world direction
|
||||
world_direction = obj.matrix_world.to_3x3() @ local_direction
|
||||
return world_direction
|
||||
|
||||
return local_direction
|
||||
return Vector([0.0, 0.0, 1.0])
|
||||
|
||||
@classmethod
|
||||
def get_local_extrusion_vector(cls, element):
|
||||
"""Get the extrusion direction in LOCAL coordinates (from IFC, no object rotation)"""
|
||||
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
local_direction = Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
return local_direction
|
||||
return Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
return Vector([0.0, 0.0, 1.0])
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1051,6 +1051,7 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
tool.Model.regenerate_array(obj, array_data)
|
||||
|
||||
array_pset = tool.Pset.get_element_pset(element, "BBIM_Array")
|
||||
json_data = tool.Ifc.get().createIfcText(json.dumps(array_data))
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=array_pset, properties={"Data": json_data})
|
||||
|
||||
@@ -1063,22 +1064,17 @@ class Model(bonsai.core.tool.Model):
|
||||
cls, parent_obj: bpy.types.Object, data: list[dict[str, Any]], array_layers_to_apply: Iterable[int] = tuple()
|
||||
) -> None:
|
||||
"""`array_layers_to_apply` - list of array layer indices to apply"""
|
||||
tool.Blender.Modifier.Array.remove_constraints(tool.Ifc.get_entity(parent_obj))
|
||||
parent_element = tool.Ifc.get_entity(parent_obj)
|
||||
|
||||
if pset := ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array"):
|
||||
ifcopenshell.api.pset.remove_pset(
|
||||
tool.Ifc.get(), product=parent_element, pset=tool.Ifc.get().by_id(pset["id"])
|
||||
)
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
obj_stack = [parent_obj]
|
||||
|
||||
for array_i, array in enumerate(data):
|
||||
# for `sync_children` we remove all previously generated children to regenerate them again
|
||||
# to assure they are in complete sync (psets, etc) with the array parent
|
||||
if array["sync_children"]:
|
||||
removed_children = set(array["children"])
|
||||
for removed_child in removed_children:
|
||||
element = tool.Ifc.get().by_guid(removed_child)
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
array["children"].clear()
|
||||
|
||||
child_i = 0
|
||||
existing_children = set(array["children"])
|
||||
total_existing_children = len(array["children"])
|
||||
@@ -1104,22 +1100,21 @@ class Model(bonsai.core.tool.Model):
|
||||
child_obj = tool.Ifc.get_object(child_element)
|
||||
assert child_obj
|
||||
except:
|
||||
old_to_new, _ = tool.Geometry.duplicate_ifc_objects([obj])
|
||||
# TODO Is this correct to assume one child? I really
|
||||
# don't understand the linked aggregates and array
|
||||
# behaviour.
|
||||
old_to_new, _ = tool.Geometry.duplicate_ifc_objects([parent_obj])
|
||||
child_element = next(iter(old_to_new.values()))[0]
|
||||
child_obj = tool.Ifc.get_object(child_element)
|
||||
|
||||
# add child pset
|
||||
child_pset = tool.Pset.get_element_pset(child_element, "BBIM_Array")
|
||||
if child_pset:
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
tool.Ifc.get(),
|
||||
pset=child_pset,
|
||||
properties={"Data": None},
|
||||
should_purge=False,
|
||||
if not (child_pset := tool.Pset.get_element_pset(child_element, "BBIM_Array")):
|
||||
child_pset = ifcopenshell.api.pset.add_pset(
|
||||
tool.Ifc.get(), product=child_element, name="BBIM_Array"
|
||||
)
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
tool.Ifc.get(),
|
||||
pset=child_pset,
|
||||
properties={"Data": None, "Parent": parent_element.GlobalId},
|
||||
should_purge=False,
|
||||
)
|
||||
|
||||
# set child object position
|
||||
new_matrix = obj.matrix_world.copy()
|
||||
@@ -1155,6 +1150,12 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=parent_element, name="BBIM_Array")
|
||||
json_data = tool.Ifc.get().createIfcText(json.dumps(data))
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
tool.Ifc.get(), pset=pset, properties={"Data": json_data, "Parent": parent_element.GlobalId}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def replace_object_ifc_representation(
|
||||
cls,
|
||||
|
||||
@@ -74,9 +74,25 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
return bpy.context.scene.BIMGridProperties
|
||||
|
||||
@classmethod
|
||||
def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool:
|
||||
if not (element := tool.Ifc.get_entity(element_obj)):
|
||||
return False
|
||||
def get_decomposition(cls, element: ifcopenshell.entity_instance) -> list(ifcopenshell.entity_instance):
|
||||
return ifcopenshell.util.element.get_decomposition(element)
|
||||
|
||||
@classmethod
|
||||
def get_root_element(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
while True:
|
||||
if parent := (
|
||||
ifcopenshell.util.element.get_aggregate(element)
|
||||
or ifcopenshell.util.element.get_nest(element)
|
||||
or ifcopenshell.util.element.get_filled_void(element)
|
||||
or ifcopenshell.util.element.get_voided_element(element)
|
||||
):
|
||||
element = parent
|
||||
else:
|
||||
break
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def can_contain(cls, container: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance) -> bool:
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
if not container.is_a("IfcSpatialStructureElement"):
|
||||
return False
|
||||
@@ -147,10 +163,10 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
|
||||
@classmethod
|
||||
def run_spatial_assign_container(
|
||||
cls, container: ifcopenshell.entity_instance, element_obj: bpy.types.Object
|
||||
cls, container: ifcopenshell.entity_instance, objs: list[bpy.types.Object]
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return bonsai.core.spatial.assign_container(
|
||||
tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj
|
||||
tool.Ifc, tool.Collector, tool.Spatial, container=container, objs=objs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -419,11 +419,6 @@ class Unit(bonsai.core.tool.Unit):
|
||||
new.is_assigned = unit in assigned_units
|
||||
new.ifc_class = unit.is_a()
|
||||
|
||||
@classmethod
|
||||
def is_scene_unit_metric(cls) -> bool:
|
||||
assert bpy.context.scene
|
||||
return bpy.context.scene.unit_settings.system in ["METRIC", "NONE"]
|
||||
|
||||
@classmethod
|
||||
def is_unit_class(cls, unit: ifcopenshell.entity_instance, ifc_class: str) -> bool:
|
||||
return unit.is_a(ifc_class)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[pytest]
|
||||
markers =
|
||||
aggregate
|
||||
array
|
||||
attribute
|
||||
boolean
|
||||
boundary
|
||||
|
||||
@@ -78,7 +78,8 @@ BONSAI_PATH = find_bonsai_path()
|
||||
# ---------------------------
|
||||
|
||||
# Never changed by user.
|
||||
PACKAGE_PATH = BLENDER_PATH / r"extensions/.local/lib/python3.11/site-packages"
|
||||
PYTHON_VERSION = "3.13" if BLENDER_VERSION == "5.1" else "3.11"
|
||||
PACKAGE_PATH = BLENDER_PATH / rf"extensions/.local/lib/python{PYTHON_VERSION}/site-packages"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -193,7 +194,7 @@ def main() -> None:
|
||||
print(f"Downloading {url} -> {filepath}")
|
||||
urllib.request.urlretrieve(url, filepath)
|
||||
|
||||
input("Dev environment is all set. 🎉🎉\nPress Enter to continue..." "")
|
||||
input("Dev environment is all set!! \nPress Enter to continue...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
@array
|
||||
Feature: Array
|
||||
|
||||
Scenario: Add array
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
When the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I see "No Array Found"
|
||||
And I click "ADD"
|
||||
Then the object "IfcColumn/Column" exists
|
||||
And I see "Column"
|
||||
And I see "1 Items"
|
||||
And I don't see "No Array Found"
|
||||
And the object "IfcColumn/Column" is at "0,0,0"
|
||||
|
||||
Scenario: Enable editing array
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
When I click "GREASEPENCIL"
|
||||
Then I see "Count"
|
||||
And I see "Method"
|
||||
And I don't see "1 Items"
|
||||
|
||||
Scenario: Disable editing array
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
When I click "CANCEL"
|
||||
Then I see "1 Items"
|
||||
And I don't see "Count"
|
||||
|
||||
Scenario: Edit array
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
When I set the "Count" property to "2"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcColumn/Column.001" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column" is at "0,0,0"
|
||||
And the object "IfcColumn/Column.001" is at "0,0,0"
|
||||
And the object "IfcColumn/Column" dimensions are "0.5,0.6,3"
|
||||
|
||||
Scenario: Edit array - offset method
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
When I set the "Count" property to "3"
|
||||
And I set the "Method" property to "Offset"
|
||||
And I set the "X" property to "1"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcColumn/Column.001" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column.002" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column" is at "0,0,0"
|
||||
And the object "IfcColumn/Column.001" is at "1,0,0"
|
||||
And the object "IfcColumn/Column.002" is at "2,0,0"
|
||||
And the object "IfcColumn/Column" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.001" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.002" dimensions are "0.5,0.6,3"
|
||||
|
||||
Scenario: Edit array - distribute method
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
When I set the "Count" property to "3"
|
||||
And I set the "Method" property to "Distribute"
|
||||
And I set the "X" property to "2"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcColumn/Column.001" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column.002" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column" is at "0,0,0"
|
||||
And the object "IfcColumn/Column.001" is at "1,0,0"
|
||||
And the object "IfcColumn/Column.002" is at "2,0,0"
|
||||
And the object "IfcColumn/Column" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.001" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.002" dimensions are "0.5,0.6,3"
|
||||
|
||||
Scenario: Edit array - local space
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is rotated by "0,0,90" deg
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
When I set the "Count" property to "2"
|
||||
And I set the "Method" property to "Offset"
|
||||
And I set the "Use Local Space" property to "TRUE"
|
||||
And I set the "X" property to "1"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcColumn/Column" is at "0,0,0"
|
||||
And the object "IfcColumn/Column.001" is at "0,1,0"
|
||||
|
||||
Scenario: Edit array - world space
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is rotated by "0,0,90" deg
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
When I set the "Count" property to "2"
|
||||
And I set the "Method" property to "Offset"
|
||||
And I set the "Use Local Space" property to "FALSE"
|
||||
And I set the "X" property to "1"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcColumn/Column" is at "0,0,0"
|
||||
And the object "IfcColumn/Column.001" is at "1,0,0"
|
||||
|
||||
Scenario: Edit array - decrease count
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
When I set the "Count" property to "3"
|
||||
And I click "CHECKMARK"
|
||||
And I click "GREASEPENCIL"
|
||||
When I set the "Count" property to "2"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcColumn/Column.001" exists
|
||||
And the object "IfcColumn/Column.002" does not exist
|
||||
|
||||
Scenario: Edit array - multiple arrays
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
When I set the "Count" property to "3"
|
||||
And I set the "Method" property to "Offset"
|
||||
And I set the "X" property to "1"
|
||||
And I click "CHECKMARK"
|
||||
And I see "3 Items"
|
||||
And I click "ADD"
|
||||
And I click the "GREASEPENCIL" after the text "1 Items (Offset)"
|
||||
And I set the "Count" property to "2"
|
||||
And I set the "Method" property to "Offset"
|
||||
And I set the "Y" property to "1"
|
||||
And I click the "CHECKMARK" after the text "Count"
|
||||
Then I see "3 Items"
|
||||
And I see "2 Items"
|
||||
Then the object "IfcColumn/Column.001" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column.002" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column.003" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column.004" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column.005" is in the collection "IfcBuildingStorey/My Storey"
|
||||
And the object "IfcColumn/Column" is at "0,0,0"
|
||||
And the object "IfcColumn/Column.001" is at "1,0,0"
|
||||
And the object "IfcColumn/Column.002" is at "2,0,0"
|
||||
And the object "IfcColumn/Column.003" is at "0,1,0"
|
||||
And the object "IfcColumn/Column.004" is at "1,1,0"
|
||||
And the object "IfcColumn/Column.005" is at "2,1,0"
|
||||
And the object "IfcColumn/Column" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.001" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.002" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.003" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.004" dimensions are "0.5,0.6,3"
|
||||
And the object "IfcColumn/Column.005" dimensions are "0.5,0.6,3"
|
||||
|
||||
Scenario: Remove array
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
And I set the "Count" property to "2"
|
||||
And I set the "Method" property to "Offset"
|
||||
And I set the "X" property to "1"
|
||||
And I click "CHECKMARK"
|
||||
When I click "X"
|
||||
Then the object "IfcColumn/Column" exists
|
||||
And the object "IfcColumn/Column.001" does not exist
|
||||
|
||||
Scenario: Regenerate array
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
And I set the "Count" property to "2"
|
||||
And I click "CHECKMARK"
|
||||
When I click "FILE_REFRESH"
|
||||
Then the object "IfcColumn/Column" exists
|
||||
And the object "IfcColumn/Column.001" exists
|
||||
|
||||
Scenario: Apply array
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
And I set the "Count" property to "2"
|
||||
And I click "CHECKMARK"
|
||||
When I click "CHECKMARK"
|
||||
Then the object "IfcColumn/Column" exists
|
||||
And the object "IfcColumn/Column.001" exists
|
||||
And I see "No Array Found"
|
||||
|
||||
Scenario: Select array parent
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
And I set the "Count" property to "2"
|
||||
And I click "CHECKMARK"
|
||||
When the object "IfcColumn/Column.001" is selected
|
||||
And I click "OBJECT_DATA"
|
||||
Then the object "IfcColumn/Column" is selected
|
||||
And the object "IfcColumn/Column.001" is not selected
|
||||
|
||||
Scenario: Select all array objects
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
|
||||
And I add the construction type
|
||||
And the object "IfcColumn/Column" is selected
|
||||
And I look at the "Array" panel
|
||||
And I click "ADD"
|
||||
And I click "GREASEPENCIL"
|
||||
And I set the "Count" property to "2"
|
||||
And I click "CHECKMARK"
|
||||
When the object "IfcColumn/Column.001" is selected
|
||||
And I click "RESTRICT_SELECT_OFF"
|
||||
Then the object "IfcColumn/Column" is selected
|
||||
And the object "IfcColumn/Column.001" is selected
|
||||
@@ -74,6 +74,50 @@ Scenario: Assign container
|
||||
When I click "CHECKMARK"
|
||||
Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site"
|
||||
|
||||
Scenario: Assign container - assign an aggregate which also affects children
|
||||
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 the object "IfcWall/Cube" is selected
|
||||
When I press "bim.add_aggregate"
|
||||
Then the object "IfcElementAssembly/Default_Name" exists
|
||||
And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey"
|
||||
When I look at the "Spatial Decomposition" panel
|
||||
And I select the "My Site" item in the "BIM_UL_containers_manager" list
|
||||
And I click "Set Default"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I look at the "Spatial Container" panel
|
||||
And I click "GREASEPENCIL"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site"
|
||||
And the object "IfcElementAssembly/Default_Name" is in the collection "IfcSite/My Site"
|
||||
|
||||
Scenario: Assign container - assign a child which also affects parents
|
||||
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 the object "IfcWall/Cube" is selected
|
||||
When I press "bim.add_aggregate"
|
||||
Then the object "IfcElementAssembly/Default_Name" exists
|
||||
And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey"
|
||||
When I look at the "Spatial Decomposition" panel
|
||||
And I select the "My Site" item in the "BIM_UL_containers_manager" list
|
||||
And I click "Set Default"
|
||||
And the object "IfcElementAssembly/Default_Name" is selected
|
||||
And I look at the "Spatial Container" panel
|
||||
And I click "GREASEPENCIL"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site"
|
||||
And the object "IfcElementAssembly/Default_Name" is in the collection "IfcSite/My Site"
|
||||
|
||||
Scenario: Copy to container
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
|
||||
@@ -110,7 +110,7 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
|
||||
And the object "IfcWall/Unnamed" has a "100" thick layered material containing the material "Default"
|
||||
And the object "IfcWall/Unnamed" dimensions are ".5,.1,.5"
|
||||
|
||||
Scenario: Assign type - assign to a different type with a material layer set
|
||||
Scenario: Assign type - assign to a different type with a LAYER2 material layer set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
@@ -149,6 +149,23 @@ Scenario: Assign type - assign to a different type with a material layer set
|
||||
Then the object "IfcWall/Cube" has a "200" thick layered material containing the material "Default"
|
||||
And the object "IfcWall/Cube" dimensions are "1,.2,1"
|
||||
|
||||
Scenario: Assign type - assign to a different type with a LAYER3 material layer set
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
When I press "bim.add_occurrence"
|
||||
Then the object "IfcSlab/Slab" is an "IfcSlab"
|
||||
And the object "IfcSlab/Slab" dimensions are "1,1,0.2"
|
||||
And the object "IfcSlab/Slab" bottom left corner is at "0,0,0"
|
||||
And the object "IfcSlab/Slab" top right corner is at "1,1,0.2"
|
||||
When I look at the "Type" panel
|
||||
And I click "GREASEPENCIL"
|
||||
And I set the "relating_type" property to "FLR300"
|
||||
And I click "CHECKMARK"
|
||||
Then the object "IfcSlab/Slab" dimensions are "1,1,0.3"
|
||||
|
||||
Scenario: Assign type - assign to a type with a material profile set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
|
||||
@@ -137,6 +137,13 @@ def misc():
|
||||
prophet.verify()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model():
|
||||
prophet = Prophecy(bonsai.core.tool.Model)
|
||||
yield prophet
|
||||
prophet.verify()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nest():
|
||||
prophet = Prophecy(bonsai.core.tool.Nest)
|
||||
|
||||
@@ -38,16 +38,19 @@ class TestDereferenceStructure:
|
||||
|
||||
class TestAssignContainer:
|
||||
def test_run(self, ifc, collector, spatial):
|
||||
spatial.can_contain("container", "element_obj").should_be_called().will_return(True)
|
||||
ifc.get_entity("element_obj").should_be_called().will_return("element")
|
||||
ifc.run(
|
||||
"spatial.assign_container", products=["element"], relating_structure="container"
|
||||
).should_be_called().will_return("rel")
|
||||
spatial.disable_editing("element_obj").should_be_called()
|
||||
collector.assign("element_obj").should_be_called()
|
||||
assert (
|
||||
subject.assign_container(ifc, collector, spatial, container="container", element_obj="element_obj") == "rel"
|
||||
)
|
||||
ifc.get_entity("obj").should_be_called().will_return("element")
|
||||
spatial.get_root_element("element").should_be_called().will_return("aggregate")
|
||||
spatial.get_decomposition("aggregate").should_be_called().will_return(["element", "element2"])
|
||||
spatial.can_contain("container", "aggregate").should_be_called().will_return(True)
|
||||
ifc.run("spatial.assign_container", products=["aggregate"], relating_structure="container").should_be_called()
|
||||
spatial.disable_editing("obj").should_be_called()
|
||||
ifc.get_object("aggregate").should_be_called().will_return("aggregate_obj")
|
||||
ifc.get_object("element").should_be_called().will_return("obj")
|
||||
ifc.get_object("element2").should_be_called().will_return("obj2")
|
||||
collector.assign("aggregate_obj").should_be_called()
|
||||
collector.assign("obj").should_be_called()
|
||||
collector.assign("obj2").should_be_called()
|
||||
subject.assign_container(ifc, collector, spatial, container="container", objs=["obj"])
|
||||
|
||||
|
||||
class TestEnableEditingContainer:
|
||||
@@ -82,7 +85,7 @@ class TestCopyToContainer:
|
||||
spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj")
|
||||
spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called()
|
||||
spatial.run_root_copy_class(obj="new_obj").should_be_called()
|
||||
spatial.run_spatial_assign_container(container="to_container", element_obj="new_obj").should_be_called()
|
||||
spatial.run_spatial_assign_container(container="to_container", objs=["new_obj"]).should_be_called()
|
||||
|
||||
spatial.disable_editing("obj").should_be_called()
|
||||
|
||||
@@ -97,7 +100,7 @@ class TestCopyToContainer:
|
||||
spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj")
|
||||
spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called()
|
||||
spatial.run_root_copy_class(obj="new_obj").should_be_called()
|
||||
spatial.run_spatial_assign_container(container="to_container", element_obj="new_obj").should_be_called()
|
||||
spatial.run_spatial_assign_container(container="to_container", objs=["new_obj"]).should_be_called()
|
||||
|
||||
spatial.disable_editing("obj").should_be_called()
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.core.type as subject
|
||||
from test.core.bootstrap import ifc, model, type, geometry
|
||||
from test.core.bootstrap import geometry, ifc, model, type
|
||||
|
||||
|
||||
class TestAssignType:
|
||||
|
||||
@@ -22,119 +22,91 @@ from test.core.bootstrap import ifc, unit
|
||||
|
||||
class TestAssignSceneUnits:
|
||||
def test_creating_and_assigning_metric_units(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix")
|
||||
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix")
|
||||
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO")
|
||||
unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return(None)
|
||||
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return(
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("volume_name")
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("mass_name")
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("time_name")
|
||||
unit.is_si_unit("length_name").should_be_called().will_return(True)
|
||||
unit.is_si_unit("area_name").should_be_called().will_return(True)
|
||||
unit.is_si_unit("volume_name").should_be_called().will_return(True)
|
||||
unit.is_si_unit("mass_name").should_be_called().will_return(True)
|
||||
unit.is_si_unit("time_name").should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("length_name").should_be_called().will_return("length_prefix")
|
||||
unit.get_scene_unit_si_prefix("area_name").should_be_called().will_return("area_prefix")
|
||||
unit.get_scene_unit_si_prefix("volume_name").should_be_called().will_return("volume_prefix")
|
||||
unit.get_scene_unit_si_prefix("mass_name").should_be_called().will_return("mass_prefix")
|
||||
unit.get_scene_unit_si_prefix("time_name").should_be_called().will_return("time_prefix")
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="length_prefix").should_be_called().will_return(
|
||||
"lengthunit"
|
||||
)
|
||||
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="prefix").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix").should_be_called().will_return(
|
||||
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="area_prefix").should_be_called().will_return(
|
||||
"areaunit"
|
||||
)
|
||||
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="volume_prefix").should_be_called().will_return(
|
||||
"volumeunit"
|
||||
)
|
||||
ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
|
||||
ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="mass_prefix").should_be_called().will_return(
|
||||
"massunit"
|
||||
)
|
||||
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix="time_prefix").should_be_called().will_return(
|
||||
"timeunit"
|
||||
)
|
||||
ifc.run(
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit"]
|
||||
).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_and_assigning_metric_units_without_mass_and_time(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("CENTI")
|
||||
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("CENTI")
|
||||
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("CENTI")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(False)
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="CENTI").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="CENTI").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="CENTI").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called()
|
||||
def test_creating_and_assigning_only_specified_units(self, ifc, unit):
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return(None)
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return(None)
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return(None)
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return(None)
|
||||
unit.is_si_unit("length_name").should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("length_name").should_be_called().will_return("length_prefix")
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="length_prefix").should_be_called().will_return(
|
||||
"lengthunit"
|
||||
)
|
||||
ifc.run("unit.assign_unit", units=["lengthunit"]).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_and_assigning_imperial_units(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(False)
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound")
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND")
|
||||
|
||||
ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="pound").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("volume_name")
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("mass_name")
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("time_name")
|
||||
unit.is_si_unit("length_name").should_be_called().will_return(False)
|
||||
unit.is_si_unit("area_name").should_be_called().will_return(False)
|
||||
unit.is_si_unit("volume_name").should_be_called().will_return(False)
|
||||
unit.is_si_unit("mass_name").should_be_called().will_return(False)
|
||||
unit.is_si_unit("time_name").should_be_called().will_return(False)
|
||||
ifc.run("unit.add_conversion_based_unit", name="length_name").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="area_name").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="volume_name").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="mass_name").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="time_name").should_be_called().will_return("timeunit")
|
||||
ifc.run(
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit"]
|
||||
).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_and_assigning_imperial_units_without_mass_and_time(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(False)
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("yard")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square yard")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic yard")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(False)
|
||||
ifc.run("unit.add_conversion_based_unit", name="yard").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="square yard").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="cubic yard").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_metric_units_with_conversion_based_mass_and_time(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("MILLI")
|
||||
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return(None)
|
||||
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return(None)
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("CONVERSION")
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("tonne")
|
||||
unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return("CONVERSION")
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute")
|
||||
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="MILLI").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=None).should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=None).should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="tonne").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
|
||||
ifc.run(
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
|
||||
).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_imperial_units_with_conversion_based_units(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(False)
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("inch")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square inch")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic inch")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("ounce")
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("hour")
|
||||
|
||||
ifc.run("unit.add_conversion_based_unit", name="inch").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="square inch").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="cubic inch").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="ounce").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="hour").should_be_called().will_return("timeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
|
||||
ifc.run(
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
|
||||
).should_be_called()
|
||||
def test_creating_both_metric_and_imperial_units(self, ifc, unit):
|
||||
# I know British doctors measure with stones so...
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("length_name")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("area_name")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return(None)
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return(None)
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return(None)
|
||||
unit.is_si_unit("length_name").should_be_called().will_return(True)
|
||||
unit.is_si_unit("area_name").should_be_called().will_return(False)
|
||||
unit.get_scene_unit_si_prefix("length_name").should_be_called().will_return("length_prefix")
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="length_prefix").should_be_called().will_return(
|
||||
"lengthunit"
|
||||
)
|
||||
ifc.run("unit.add_conversion_based_unit", name="area_name").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.assign_unit", units=["lengthunit", "areaunit"]).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
|
||||
|
||||
@@ -323,8 +323,8 @@ class TestRecordObjectPosition(NewFile):
|
||||
obj = bpy.data.objects.new("Object", None)
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
subject.record_object_position(obj)
|
||||
assert props.location_checksum == repr(np.array(obj.matrix_world.translation, dtype=np.float32).tobytes())
|
||||
assert props.rotation_checksum == repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
|
||||
assert props.location_checksum == repr(tool.Blender.np_array_legacy(obj.matrix_world.translation).tobytes())
|
||||
assert props.rotation_checksum == repr(tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()).tobytes())
|
||||
|
||||
|
||||
class TestRemoveConnection(NewFile):
|
||||
|
||||
@@ -43,9 +43,7 @@ class TestCanContain(NewFile):
|
||||
structure_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(structure, structure_obj)
|
||||
element = ifc.createIfcWall()
|
||||
element_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(element, element_obj)
|
||||
assert subject.can_contain(structure, element_obj) is True
|
||||
assert subject.can_contain(structure, element) is True
|
||||
|
||||
def test_a_spatial_structure_element_can_contain_an_element_ifc2x3(self):
|
||||
ifc = ifcopenshell.file(schema="IFC2X3")
|
||||
@@ -54,9 +52,7 @@ class TestCanContain(NewFile):
|
||||
structure_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(structure, structure_obj)
|
||||
element = ifc.createIfcWall()
|
||||
element_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(element, element_obj)
|
||||
assert subject.can_contain(structure, element_obj) is True
|
||||
assert subject.can_contain(structure, element) is True
|
||||
|
||||
def test_a_spatial_zone_element_cannot_contain_an_element(self):
|
||||
ifc = ifcopenshell.file()
|
||||
@@ -65,14 +61,7 @@ class TestCanContain(NewFile):
|
||||
structure_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(structure, structure_obj)
|
||||
element = ifc.createIfcWall()
|
||||
element_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(element, element_obj)
|
||||
assert subject.can_contain(structure, element_obj) is False
|
||||
|
||||
def test_unlinked_elements_cannot_contain_anything(self):
|
||||
structure_obj = bpy.data.objects.new("Object", None)
|
||||
element_obj = bpy.data.objects.new("Object", None)
|
||||
assert subject.can_contain(structure_obj, element_obj) is False
|
||||
assert subject.can_contain(structure, element) is False
|
||||
|
||||
def test_a_non_spatial_element_cannot_contain_anything(self):
|
||||
ifc = ifcopenshell.file()
|
||||
@@ -81,9 +70,7 @@ class TestCanContain(NewFile):
|
||||
structure_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(structure, structure_obj)
|
||||
element = ifc.createIfcWall()
|
||||
element_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(element, element_obj)
|
||||
assert subject.can_contain(structure, element_obj) is False
|
||||
assert subject.can_contain(structure, element) is False
|
||||
|
||||
def test_a_non_element_cannot_be_contained(self):
|
||||
ifc = ifcopenshell.file()
|
||||
@@ -92,9 +79,7 @@ class TestCanContain(NewFile):
|
||||
structure_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(structure, structure_obj)
|
||||
element = ifc.createIfcTask()
|
||||
element_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(element, element_obj)
|
||||
assert subject.can_contain(structure, element_obj) is False
|
||||
assert subject.can_contain(structure, element) is False
|
||||
|
||||
def test_other_non_elements_that_have_a_contained_in_structure_attribute_can_be_contained(self):
|
||||
ifc = ifcopenshell.file()
|
||||
@@ -103,9 +88,7 @@ class TestCanContain(NewFile):
|
||||
structure_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(structure, structure_obj)
|
||||
element = ifc.createIfcGrid()
|
||||
element_obj = bpy.data.objects.new("Object", None)
|
||||
tool.Ifc.link(element, element_obj)
|
||||
assert subject.can_contain(structure, element_obj) is True
|
||||
assert subject.can_contain(structure, element) is True
|
||||
|
||||
|
||||
class TestCanReference(NewFile):
|
||||
|
||||
@@ -434,18 +434,6 @@ class TestImportUnits(NewFile):
|
||||
assert second_prop.ifc_class == "IfcSIUnit"
|
||||
|
||||
|
||||
class TestIsSceneUnitMetric(NewFile):
|
||||
def test_run(self):
|
||||
assert bpy.context.scene
|
||||
props = bpy.context.scene.unit_settings
|
||||
props.system = "METRIC"
|
||||
assert subject.is_scene_unit_metric() is True
|
||||
props.system = "IMPERIAL"
|
||||
assert subject.is_scene_unit_metric() is False
|
||||
props.system = "NONE"
|
||||
assert subject.is_scene_unit_metric() is True
|
||||
|
||||
|
||||
class TestIsUnitClass:
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
#include "ifcparse/Ifc2x3.h"
|
||||
#include "ifcparse/IfcHierarchyHelper.h"
|
||||
|
||||
#include <boost/math/constants/constants.hpp>
|
||||
const static double PI = boost::math::constants::pi<double>();
|
||||
|
||||
typedef std::string S;
|
||||
typedef IfcParse::IfcGlobalId guid;
|
||||
boost::none_t const null = boost::none;
|
||||
@@ -41,7 +44,7 @@ void create_curve_rebar(IfcHierarchyHelper<IfcSchema>& file)
|
||||
int R = 3 * dia;
|
||||
int length = 12 * dia;
|
||||
|
||||
double crossSectionarea = M_PI * (dia / 2) * 2;
|
||||
double crossSectionarea = PI * dia * dia / 4;
|
||||
IfcSchema::IfcReinforcingBar* rebar = new IfcSchema::IfcReinforcingBar(
|
||||
guid(), 0, S("test"), null,
|
||||
null, 0, 0,
|
||||
|
||||
@@ -15,6 +15,7 @@ target_link_libraries(
|
||||
IfcGeom
|
||||
IfcParse
|
||||
Serializers
|
||||
${kernel_libraries}
|
||||
${OpenCASCADE_LIBRARIES}
|
||||
${Boost_LIBRARIES}
|
||||
${HDF5_LIBRARIES}
|
||||
|
||||
@@ -452,6 +452,12 @@ namespace ifcopenshell {
|
||||
static constexpr bool defaultvalue = false;
|
||||
};
|
||||
|
||||
struct MakeVolume : public SettingBase<MakeVolume, bool> {
|
||||
static constexpr const char* const name = "make-volume";
|
||||
static constexpr const char* const description = "Try to isolate and fix a valid volume from non-manifold elements prior to opening subtraction";
|
||||
static constexpr bool defaultvalue = false;
|
||||
};
|
||||
|
||||
struct DeferProcessingFirstElement : public SettingBase<DeferProcessingFirstElement, bool, true> {
|
||||
static constexpr const char* const name = "defer-processing-first-element";
|
||||
static constexpr const char* const description = "Don't process first element in Iterator::initialize call()";
|
||||
@@ -647,7 +653,7 @@ namespace ifcopenshell {
|
||||
};
|
||||
|
||||
class Settings : public SettingsContainer<
|
||||
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset>
|
||||
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset, MakeVolume>
|
||||
>
|
||||
{};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "base_utils.h"
|
||||
|
||||
#include <BRepPrimAPI_MakeRevol.hxx>
|
||||
#include <BOPAlgo_MakerVolume.hxx>
|
||||
|
||||
namespace {
|
||||
struct opening_sorter {
|
||||
@@ -132,11 +133,24 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
parts.push_back(it3_shape);
|
||||
}
|
||||
|
||||
for (auto& entity_part : parts) {
|
||||
for (auto entity_part : parts) {
|
||||
bool is_manifold = util::is_manifold(entity_part);
|
||||
|
||||
if (!is_manifold) {
|
||||
Logger::Warning("Non-manifold first operand");
|
||||
if (settings_.get<settings::MakeVolume>().get()) {
|
||||
BOPAlgo_MakerVolume mv;
|
||||
mv.AddArgument(entity_part);
|
||||
mv.SetAvoidInternalShapes(true);
|
||||
try {
|
||||
mv.Perform();
|
||||
entity_part = mv.Shape();
|
||||
Logger::Warning("Sucessfully detected exterior volume to non-manifold first operand");
|
||||
} catch (const Standard_Failure& e) {
|
||||
Logger::Warning("MakeVolume failed: " + std::string(e.GetMessageString()), entity);
|
||||
}
|
||||
} else {
|
||||
Logger::Warning("Non-manifold first operand, use --make-volume to try and make manifold");
|
||||
}
|
||||
}
|
||||
|
||||
TopoDS_Shape entity_part_result;
|
||||
|
||||
+6
-1
@@ -28,7 +28,7 @@ def _create_offset_curve_representation(
|
||||
file: ifcopenshell.file, alignment: entity_instance, offsets: Sequence[entity_instance]
|
||||
) -> None:
|
||||
"""
|
||||
Create geometric representation for the alignment based on an IfcPolyline
|
||||
Create geometric representation for the alignment based on an IfcOffsetByDistances curve
|
||||
|
||||
:param alignment: The alignment for which the representation is being created
|
||||
:return: None
|
||||
@@ -36,6 +36,11 @@ def _create_offset_curve_representation(
|
||||
expected_type = "IfcAlignment"
|
||||
if not alignment.is_a(expected_type):
|
||||
raise TypeError(f"Expected {expected_type} but got {alignment.is_a()}")
|
||||
|
||||
expected_type = "IfcPointByDistanceExpression"
|
||||
for offset in offsets:
|
||||
if not offset.is_a(expected_type):
|
||||
raise TypeError(f"Expected {expected_type} but got {offset.is_a()}")
|
||||
|
||||
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ def create_as_offset_curve(
|
||||
|
||||
:param file:
|
||||
:param name: name assigned to IfcAlignment.Name
|
||||
:param offsets: offsets from the basis curve that defines the offset curve, expected to be IfcOffsetCurveByDistances.
|
||||
:param offsets: offsets from the basis curve that defines the offset curve, expected to be IfcPointByDistanceExpression.
|
||||
:param start_station: station value at the start of the alignment
|
||||
:return: Returns an IfcAlignment
|
||||
"""
|
||||
|
||||
@@ -100,15 +100,10 @@ class Usecase:
|
||||
size = self.convert_si_to_unit(1)
|
||||
points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0))
|
||||
if self.polyline:
|
||||
# Only scale polyline if we have actual slope
|
||||
if self.x_angle and abs(self.x_angle) > 1e-6:
|
||||
points = [
|
||||
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
|
||||
for p in self.polyline
|
||||
]
|
||||
else:
|
||||
points = [(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1])) for p in self.polyline]
|
||||
|
||||
points = [
|
||||
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
|
||||
for p in self.polyline
|
||||
]
|
||||
if self.file.schema == "IFC2X3":
|
||||
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
|
||||
else:
|
||||
@@ -119,23 +114,21 @@ class Usecase:
|
||||
else:
|
||||
direction_ratios = (0.0, 0.0, 1.0)
|
||||
|
||||
offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative
|
||||
extrusion_direction = self.file.createIfcDirection(direction_ratios)
|
||||
if self.direction_sense == "NEGATIVE":
|
||||
direction_ratios = tuple(-n for n in direction_ratios)
|
||||
extrusion_direction = self.file.createIfcDirection(direction_ratios)
|
||||
|
||||
# Calculate depth based on extrusion angle
|
||||
extrusion_angle = abs(self.x_angle) if self.x_angle else 0
|
||||
if extrusion_angle > 1e-6:
|
||||
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(extrusion_angle))
|
||||
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
perpendicular_depth = self.convert_si_to_unit(self.depth)
|
||||
perpendicular_offset = self.convert_si_to_unit(self.offset)
|
||||
|
||||
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(self.x_angle))
|
||||
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(self.x_angle))
|
||||
position = None
|
||||
# default position for IFC2X3 where .Position is not optional
|
||||
if self.file.schema == "IFC2X3" or self.offset != 0:
|
||||
position_vector = (
|
||||
direction_ratios[0] * perpendicular_offset,
|
||||
direction_ratios[1] * perpendicular_offset,
|
||||
direction_ratios[2] * perpendicular_offset,
|
||||
offset_direction[0] * perpendicular_offset,
|
||||
offset_direction[1] * perpendicular_offset,
|
||||
offset_direction[2] * perpendicular_offset,
|
||||
)
|
||||
position = self.file.createIfcAxis2Placement3D(
|
||||
self.file.createIfcCartesianPoint(position_vector),
|
||||
|
||||
@@ -85,6 +85,7 @@ class Usecase:
|
||||
def create_item(self) -> ifcopenshell.entity_instance:
|
||||
length = self.convert_si_to_unit(self.settings["length"])
|
||||
thickness = self.convert_si_to_unit(self.settings["thickness"])
|
||||
thickness *= 1 / cos(self.settings["x_angle"])
|
||||
if self.settings["direction_sense"] == "NEGATIVE":
|
||||
thickness *= -1
|
||||
points = (
|
||||
@@ -112,7 +113,7 @@ class Usecase:
|
||||
self.file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
),
|
||||
extrusion_direction,
|
||||
self.convert_si_to_unit(self.settings["height"]),
|
||||
self.convert_si_to_unit(self.settings["height"]) * abs(1 / cos(self.settings["x_angle"])),
|
||||
)
|
||||
if self.settings["booleans"]:
|
||||
extrusion = self.apply_booleans(extrusion)
|
||||
|
||||
@@ -144,7 +144,7 @@ format_grammar = lark.Lark(
|
||||
| mul_div "*" function -> multiply
|
||||
| mul_div "/" function -> divide
|
||||
|
||||
function: round | number | int | format_length | lower | upper | title | concat | substr | variable | ESCAPED_STRING | NUMBER | "(" expression ")"
|
||||
function: round | number | int | format_length | lower | upper | title | concat | substr | variable | ESCAPED_STRING | SIGNED_NUMBER | "(" expression ")"
|
||||
|
||||
variable: "{{" query_path "}}"
|
||||
query_path: /[^}]+/
|
||||
|
||||
@@ -54,6 +54,7 @@ class TestFormat:
|
||||
def test_number_formatting(self):
|
||||
assert subject.format("round(123, 5)") == "125"
|
||||
assert subject.format('round("123", 5)') == "125"
|
||||
assert subject.format('round(-123, 5)') == "-125"
|
||||
assert subject.format("int(123.123)") == "123"
|
||||
assert subject.format("int(123)") == "123"
|
||||
assert subject.format("number(123)") == "123"
|
||||
|
||||
Generated
+3
-3
@@ -2851,9 +2851,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
|
||||
"integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
||||
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -140,6 +140,7 @@ else()
|
||||
target_link_libraries(ifcopenshell_wrapper PRIVATE ${IFCOPENSHELL_LIBRARIES} ${LIBSVGFILL})
|
||||
endif()
|
||||
target_link_libraries(ifcopenshell_wrapper PRIVATE ${CGAL_LIBRARIES})
|
||||
target_link_libraries(ifcopenshell_wrapper PRIVATE IfcGeom ${kernel_libraries})
|
||||
if((NOT WIN32) AND BUILD_SHARED_LIBS)
|
||||
SET_INSTALL_RPATHS(ifcopenshell_wrapper "${IFCDIRS};${OCC_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
Reference in New Issue
Block a user