mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-20 04:04:00 +00:00
Reimplement feature to reassign inherited containers if you select a child element
This reimplements @theoryshaw 's commit 9adbd4 but has a few upgrades: - Considers all parent / child relationships, not just aggregates - Puts business logic in core where it belongs and tool code in tool - Uses existing utils where possible like get_decomposition - Does not use name based collection checking which is fragile - Reuses tool.Collector - Makes container assignment handle the API's capability to do things in bulk instead of one by one in a loop, so it's faster - Tests
This commit is contained in:
@@ -224,7 +224,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
tool.Collector,
|
tool.Collector,
|
||||||
tool.Spatial,
|
tool.Spatial,
|
||||||
container=current_container,
|
container=current_container,
|
||||||
element_obj=aggregate,
|
objs=[aggregate],
|
||||||
)
|
)
|
||||||
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
|
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
|
||||||
|
|
||||||
|
|||||||
@@ -1730,7 +1730,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
tool.Collector,
|
tool.Collector,
|
||||||
tool.Spatial,
|
tool.Spatial,
|
||||||
container=original_data[matching_group_id][index]["Container"],
|
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)):
|
for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)):
|
||||||
tool.Collector.assign(tool.Ifc.get_object(part))
|
tool.Collector.assign(tool.Ifc.get_object(part))
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
parent = ifcopenshell.util.element.get_container(building_element)
|
parent = ifcopenshell.util.element.get_container(building_element)
|
||||||
if parent:
|
if parent:
|
||||||
bonsai.core.spatial.assign_container(
|
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
|
# set occurrences properties for the types defined with modifiers
|
||||||
@@ -493,7 +493,7 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
else:
|
else:
|
||||||
if self.container_obj:
|
if self.container_obj:
|
||||||
bonsai.core.spatial.assign_container(
|
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":
|
if props.rl_mode == "BOTTOM":
|
||||||
obj.location.z = self.container_obj.location.z - tool.Blender.get_object_bounding_box(obj)["min_z"]
|
obj.location.z = self.container_obj.location.z - tool.Blender.get_object_bounding_box(obj)["min_z"]
|
||||||
|
|||||||
@@ -171,28 +171,9 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
objs: list[bpy.types.Object] = []
|
core.assign_container(
|
||||||
# In IFC element can be either contained of aggregated,
|
tool.Ifc, tool.Collector, tool.Spatial, container=container, objs=tool.Blender.get_selected_objects()
|
||||||
# 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:
|
|
||||||
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}")
|
|
||||||
|
|
||||||
|
|
||||||
class EnableEditingContainer(bpy.types.Operator):
|
class EnableEditingContainer(bpy.types.Operator):
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ def poll_container_obj(self: "BIMObjectSpatialProperties", container_obj: bpy.ty
|
|||||||
obj = self.id_data
|
obj = self.id_data
|
||||||
if (
|
if (
|
||||||
(container := tool.Ifc.get_entity(container_obj))
|
(container := tool.Ifc.get_entity(container_obj))
|
||||||
and (tool.Ifc.get_entity(obj))
|
and (element := tool.Ifc.get_entity(obj))
|
||||||
and tool.Spatial.can_contain(container, obj)
|
and tool.Spatial.can_contain(container, element)
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -52,15 +52,22 @@ def assign_container(
|
|||||||
collector: type[tool.Collector],
|
collector: type[tool.Collector],
|
||||||
spatial: type[tool.Spatial],
|
spatial: type[tool.Spatial],
|
||||||
container: ifcopenshell.entity_instance,
|
container: ifcopenshell.entity_instance,
|
||||||
element_obj: Optional[bpy.types.Object] = None,
|
objs: Optional[bpy.types.Object] = None,
|
||||||
) -> Union[ifcopenshell.entity_instance, None]:
|
) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
if not spatial.can_contain(container, element_obj):
|
root_elements = set()
|
||||||
return
|
all_elements = set()
|
||||||
assert element_obj # Type checker.
|
for obj in objs:
|
||||||
rel = ifc.run("spatial.assign_container", products=[ifc.get_entity(element_obj)], relating_structure=container)
|
if not (element := ifc.get_entity(obj)):
|
||||||
spatial.disable_editing(element_obj)
|
continue
|
||||||
collector.assign(element_obj)
|
root_element = spatial.get_root_element(element)
|
||||||
return rel
|
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:
|
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)
|
copied_obj = spatial.duplicate_object_and_data(obj)
|
||||||
spatial.set_relative_object_matrix(copied_obj, to_container_obj, matrix)
|
spatial.set_relative_object_matrix(copied_obj, to_container_obj, matrix)
|
||||||
result_objs.append(spatial.run_root_copy_class(obj=copied_obj))
|
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)
|
spatial.disable_editing(obj)
|
||||||
return result_objs
|
return result_objs
|
||||||
|
|
||||||
|
|||||||
@@ -954,12 +954,14 @@ class Spatial:
|
|||||||
def get_object_matrix(cls, obj): pass
|
def get_object_matrix(cls, obj): pass
|
||||||
def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass
|
def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass
|
||||||
def get_selected_product_types(cls): pass
|
def get_selected_product_types(cls): pass
|
||||||
|
def get_root_element(cls, element): pass
|
||||||
|
def get_decomposition(cls, element): pass
|
||||||
def get_selected_products(cls): pass
|
def get_selected_products(cls): pass
|
||||||
def import_spatial_decomposition(cls): pass
|
def import_spatial_decomposition(cls): pass
|
||||||
def import_spatial_element(cls, element, level_index): pass
|
def import_spatial_element(cls, element, level_index): pass
|
||||||
def load_contained_elements(cls): pass
|
def load_contained_elements(cls): pass
|
||||||
def run_root_copy_class(cls, obj): 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 run_spatial_import_spatial_decomposition(cls): pass
|
||||||
def select_object(cls, obj): pass
|
def select_object(cls, obj): pass
|
||||||
def select_products(cls, products, unhide=False): pass
|
def select_products(cls, products, unhide=False): pass
|
||||||
|
|||||||
@@ -74,9 +74,25 @@ class Spatial(bonsai.core.tool.Spatial):
|
|||||||
return bpy.context.scene.BIMGridProperties
|
return bpy.context.scene.BIMGridProperties
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool:
|
def get_decomposition(cls, element: ifcopenshell.entity_instance) -> list(ifcopenshell.entity_instance):
|
||||||
if not (element := tool.Ifc.get_entity(element_obj)):
|
return ifcopenshell.util.element.get_decomposition(element)
|
||||||
return False
|
|
||||||
|
@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 tool.Ifc.get_schema() == "IFC2X3":
|
||||||
if not container.is_a("IfcSpatialStructureElement"):
|
if not container.is_a("IfcSpatialStructureElement"):
|
||||||
return False
|
return False
|
||||||
@@ -147,10 +163,10 @@ class Spatial(bonsai.core.tool.Spatial):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run_spatial_assign_container(
|
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]:
|
) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
return bonsai.core.spatial.assign_container(
|
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
|
@classmethod
|
||||||
|
|||||||
@@ -38,16 +38,19 @@ class TestDereferenceStructure:
|
|||||||
|
|
||||||
class TestAssignContainer:
|
class TestAssignContainer:
|
||||||
def test_run(self, ifc, collector, spatial):
|
def test_run(self, ifc, collector, spatial):
|
||||||
spatial.can_contain("container", "element_obj").should_be_called().will_return(True)
|
ifc.get_entity("obj").should_be_called().will_return("element")
|
||||||
ifc.get_entity("element_obj").should_be_called().will_return("element")
|
spatial.get_root_element("element").should_be_called().will_return("aggregate")
|
||||||
ifc.run(
|
spatial.get_decomposition("aggregate").should_be_called().will_return(["element", "element2"])
|
||||||
"spatial.assign_container", products=["element"], relating_structure="container"
|
spatial.can_contain("container", "aggregate").should_be_called().will_return(True)
|
||||||
).should_be_called().will_return("rel")
|
ifc.run("spatial.assign_container", products=["aggregate"], relating_structure="container").should_be_called()
|
||||||
spatial.disable_editing("element_obj").should_be_called()
|
spatial.disable_editing("obj").should_be_called()
|
||||||
collector.assign("element_obj").should_be_called()
|
ifc.get_object("aggregate").should_be_called().will_return("aggregate_obj")
|
||||||
assert (
|
ifc.get_object("element").should_be_called().will_return("obj")
|
||||||
subject.assign_container(ifc, collector, spatial, container="container", element_obj="element_obj") == "rel"
|
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:
|
class TestEnableEditingContainer:
|
||||||
@@ -82,7 +85,7 @@ class TestCopyToContainer:
|
|||||||
spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj")
|
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.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_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()
|
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.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.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_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()
|
spatial.disable_editing("obj").should_be_called()
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,7 @@ class TestCanContain(NewFile):
|
|||||||
structure_obj = bpy.data.objects.new("Object", None)
|
structure_obj = bpy.data.objects.new("Object", None)
|
||||||
tool.Ifc.link(structure, structure_obj)
|
tool.Ifc.link(structure, structure_obj)
|
||||||
element = ifc.createIfcWall()
|
element = ifc.createIfcWall()
|
||||||
element_obj = bpy.data.objects.new("Object", None)
|
assert subject.can_contain(structure, element) is True
|
||||||
tool.Ifc.link(element, element_obj)
|
|
||||||
assert subject.can_contain(structure, element_obj) is True
|
|
||||||
|
|
||||||
def test_a_spatial_structure_element_can_contain_an_element_ifc2x3(self):
|
def test_a_spatial_structure_element_can_contain_an_element_ifc2x3(self):
|
||||||
ifc = ifcopenshell.file(schema="IFC2X3")
|
ifc = ifcopenshell.file(schema="IFC2X3")
|
||||||
@@ -54,9 +52,7 @@ class TestCanContain(NewFile):
|
|||||||
structure_obj = bpy.data.objects.new("Object", None)
|
structure_obj = bpy.data.objects.new("Object", None)
|
||||||
tool.Ifc.link(structure, structure_obj)
|
tool.Ifc.link(structure, structure_obj)
|
||||||
element = ifc.createIfcWall()
|
element = ifc.createIfcWall()
|
||||||
element_obj = bpy.data.objects.new("Object", None)
|
assert subject.can_contain(structure, element) is True
|
||||||
tool.Ifc.link(element, element_obj)
|
|
||||||
assert subject.can_contain(structure, element_obj) is True
|
|
||||||
|
|
||||||
def test_a_spatial_zone_element_cannot_contain_an_element(self):
|
def test_a_spatial_zone_element_cannot_contain_an_element(self):
|
||||||
ifc = ifcopenshell.file()
|
ifc = ifcopenshell.file()
|
||||||
@@ -65,14 +61,7 @@ class TestCanContain(NewFile):
|
|||||||
structure_obj = bpy.data.objects.new("Object", None)
|
structure_obj = bpy.data.objects.new("Object", None)
|
||||||
tool.Ifc.link(structure, structure_obj)
|
tool.Ifc.link(structure, structure_obj)
|
||||||
element = ifc.createIfcWall()
|
element = ifc.createIfcWall()
|
||||||
element_obj = bpy.data.objects.new("Object", None)
|
assert subject.can_contain(structure, element) is False
|
||||||
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
|
|
||||||
|
|
||||||
def test_a_non_spatial_element_cannot_contain_anything(self):
|
def test_a_non_spatial_element_cannot_contain_anything(self):
|
||||||
ifc = ifcopenshell.file()
|
ifc = ifcopenshell.file()
|
||||||
@@ -81,9 +70,7 @@ class TestCanContain(NewFile):
|
|||||||
structure_obj = bpy.data.objects.new("Object", None)
|
structure_obj = bpy.data.objects.new("Object", None)
|
||||||
tool.Ifc.link(structure, structure_obj)
|
tool.Ifc.link(structure, structure_obj)
|
||||||
element = ifc.createIfcWall()
|
element = ifc.createIfcWall()
|
||||||
element_obj = bpy.data.objects.new("Object", None)
|
assert subject.can_contain(structure, element) is False
|
||||||
tool.Ifc.link(element, element_obj)
|
|
||||||
assert subject.can_contain(structure, element_obj) is False
|
|
||||||
|
|
||||||
def test_a_non_element_cannot_be_contained(self):
|
def test_a_non_element_cannot_be_contained(self):
|
||||||
ifc = ifcopenshell.file()
|
ifc = ifcopenshell.file()
|
||||||
@@ -92,9 +79,7 @@ class TestCanContain(NewFile):
|
|||||||
structure_obj = bpy.data.objects.new("Object", None)
|
structure_obj = bpy.data.objects.new("Object", None)
|
||||||
tool.Ifc.link(structure, structure_obj)
|
tool.Ifc.link(structure, structure_obj)
|
||||||
element = ifc.createIfcTask()
|
element = ifc.createIfcTask()
|
||||||
element_obj = bpy.data.objects.new("Object", None)
|
assert subject.can_contain(structure, element) is False
|
||||||
tool.Ifc.link(element, element_obj)
|
|
||||||
assert subject.can_contain(structure, element_obj) is False
|
|
||||||
|
|
||||||
def test_other_non_elements_that_have_a_contained_in_structure_attribute_can_be_contained(self):
|
def test_other_non_elements_that_have_a_contained_in_structure_attribute_can_be_contained(self):
|
||||||
ifc = ifcopenshell.file()
|
ifc = ifcopenshell.file()
|
||||||
@@ -103,9 +88,7 @@ class TestCanContain(NewFile):
|
|||||||
structure_obj = bpy.data.objects.new("Object", None)
|
structure_obj = bpy.data.objects.new("Object", None)
|
||||||
tool.Ifc.link(structure, structure_obj)
|
tool.Ifc.link(structure, structure_obj)
|
||||||
element = ifc.createIfcGrid()
|
element = ifc.createIfcGrid()
|
||||||
element_obj = bpy.data.objects.new("Object", None)
|
assert subject.can_contain(structure, element) is True
|
||||||
tool.Ifc.link(element, element_obj)
|
|
||||||
assert subject.can_contain(structure, element_obj) is True
|
|
||||||
|
|
||||||
|
|
||||||
class TestCanReference(NewFile):
|
class TestCanReference(NewFile):
|
||||||
|
|||||||
Reference in New Issue
Block a user