mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 18:21:59 +00:00
Append aggregates with their parts, sub-aggregates and openings (#5909)
bim.append_library_element_by_query only ever brought in the top-level
element of an assembly. Two layers needed fixing:
ifcopenshell.api.project.append_asset never whitelisted IsDecomposedBy
for IfcElement, so IfcRelAggregates was not walked, and even then the
is_another_asset guard filtered aggregated children because they share
the IfcProduct target class with the top asset. IfcRelAggregates is now
treated as a dependent relationship, the same way IfcRelVoidsElement and
IfcRelProjectsElement already are (precedent: 55e97fd170). Scoped to
IfcElement so spatial decomposition is untouched.
The Bonsai AppendLibraryElement operator then only created a viewport
object for the single top element, leaving appended parts invisible in
the 3D view. It now creates objects, materials, styles and types for
every part and sub-assembly via get_decomposition, filtering openings
the same way normal import does.
This change was written with AI assistance.
This commit is contained in:
@@ -637,10 +637,12 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
elif element.is_a("IfcProduct"):
|
elif element.is_a("IfcProduct"):
|
||||||
# NOTE: Non-types are not exposed in UI directly
|
# NOTE: Non-types are not exposed in UI directly
|
||||||
# but the code is still used when appending products by query.
|
# but the code is still used when appending products by query.
|
||||||
self.import_product_from_ifc(element, context)
|
elements = self.get_appended_elements(element)
|
||||||
element_type = ifcopenshell.util.element.get_type(element)
|
self.import_product_from_ifc(elements, context)
|
||||||
if element_type is not None and tool.Ifc.get_object(element_type) is None:
|
for appended_element in elements:
|
||||||
self.import_type_from_ifc(element_type, context)
|
element_type = ifcopenshell.util.element.get_type(appended_element)
|
||||||
|
if element_type is not None and tool.Ifc.get_object(element_type) is None:
|
||||||
|
self.import_type_from_ifc(element_type, context)
|
||||||
elif element.is_a("IfcMaterial"):
|
elif element.is_a("IfcMaterial"):
|
||||||
self.import_material_from_ifc(element, context)
|
self.import_material_from_ifc(element, context)
|
||||||
elif element.is_a("IfcSurfaceStyle"):
|
elif element.is_a("IfcSurfaceStyle"):
|
||||||
@@ -676,17 +678,32 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ifc_importer.file = self.file
|
ifc_importer.file = self.file
|
||||||
ifc_importer.create_style(style)
|
ifc_importer.create_style(style)
|
||||||
|
|
||||||
def import_product_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
|
def get_appended_elements(self, element: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
|
||||||
|
"""``element`` plus any parts brought in with it (e.g. the parts of an appended assembly)."""
|
||||||
|
elements = {element}
|
||||||
|
for part in ifcopenshell.util.element.get_decomposition(element):
|
||||||
|
if not part.is_a("IfcFeatureElement") or part.is_a("IfcSurfaceFeature"):
|
||||||
|
elements.add(part)
|
||||||
|
return elements
|
||||||
|
|
||||||
|
def import_product_from_ifc(
|
||||||
|
self,
|
||||||
|
elements: Union[ifcopenshell.entity_instance, set[ifcopenshell.entity_instance]],
|
||||||
|
context: bpy.types.Context,
|
||||||
|
) -> None:
|
||||||
self.file = tool.Ifc.get()
|
self.file = tool.Ifc.get()
|
||||||
|
if isinstance(elements, ifcopenshell.entity_instance):
|
||||||
|
elements = {elements}
|
||||||
logger = logging.getLogger("ImportIFC")
|
logger = logging.getLogger("ImportIFC")
|
||||||
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
|
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
|
||||||
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
|
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
|
||||||
ifc_importer.file = self.file
|
ifc_importer.file = self.file
|
||||||
ifc_importer.process_context_filter()
|
ifc_importer.process_context_filter()
|
||||||
ifc_importer.material_creator.load_existing_materials()
|
ifc_importer.material_creator.load_existing_materials()
|
||||||
self.import_materials(element, ifc_importer)
|
for element in elements:
|
||||||
self.import_styles(element, ifc_importer)
|
self.import_materials(element, ifc_importer)
|
||||||
ifc_importer.create_generic_elements({element})
|
self.import_styles(element, ifc_importer)
|
||||||
|
ifc_importer.create_generic_elements(elements)
|
||||||
ifc_importer.place_objects_in_collections()
|
ifc_importer.place_objects_in_collections()
|
||||||
|
|
||||||
def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
|
def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
|
||||||
@@ -2772,10 +2789,12 @@ class AppendInspectedLinkedElement(AppendLibraryElement):
|
|||||||
library=linked_ifc_file,
|
library=linked_ifc_file,
|
||||||
element=element_to_append,
|
element=element_to_append,
|
||||||
)
|
)
|
||||||
self.import_product_from_ifc(element, context)
|
elements = self.get_appended_elements(element)
|
||||||
element_type = ifcopenshell.util.element.get_type(element)
|
self.import_product_from_ifc(elements, context)
|
||||||
if element_type and tool.Ifc.get_object(element_type) is None:
|
for appended_element in elements:
|
||||||
self.import_type_from_ifc(element_type, context)
|
element_type = ifcopenshell.util.element.get_type(appended_element)
|
||||||
|
if element_type and tool.Ifc.get_object(element_type) is None:
|
||||||
|
self.import_type_from_ifc(element_type, context)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ class Usecase:
|
|||||||
self.whitelisted_inverse_attributes = {
|
self.whitelisted_inverse_attributes = {
|
||||||
"IfcObjectDefinition": ["HasAssociations"],
|
"IfcObjectDefinition": ["HasAssociations"],
|
||||||
"IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"],
|
"IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"],
|
||||||
"IfcElement": ["HasOpenings"],
|
"IfcElement": ["HasOpenings", "IsDecomposedBy"],
|
||||||
"IfcDistributionElement": ["IsNestedBy"],
|
"IfcDistributionElement": ["IsNestedBy"],
|
||||||
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
|
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
|
||||||
"IfcRepresentationItem": [
|
"IfcRepresentationItem": [
|
||||||
@@ -529,24 +529,18 @@ class Usecase:
|
|||||||
new = self.file.create_entity(element.is_a())
|
new = self.file.create_entity(element.is_a())
|
||||||
self.reuse_identities[element_identity] = new
|
self.reuse_identities[element_identity] = new
|
||||||
|
|
||||||
|
# Void, projection, and aggregation relationships are "dependent" and always considered.
|
||||||
|
is_dependent_rel = element.is_a() in ("IfcRelVoidsElement", "IfcRelProjectsElement", "IfcRelAggregates")
|
||||||
|
|
||||||
for i, attribute in enumerate(element):
|
for i, attribute in enumerate(element):
|
||||||
new_attribute = None
|
new_attribute = None
|
||||||
if isinstance(attribute, ifcopenshell.entity_instance):
|
if isinstance(attribute, ifcopenshell.entity_instance):
|
||||||
# Void and projection relationships are special because they
|
if is_dependent_rel or not self.is_another_asset(attribute):
|
||||||
# are "dependent" relationships, so we always consider them.
|
|
||||||
# We do _not_ whitelist (i.e. in is_another_asset)
|
|
||||||
# IfcFeatureElement because you can have things like
|
|
||||||
# IfcRelAssociatesClassification to openings! We only ever want
|
|
||||||
# to consider IfcFeatureElements in IfcRelVoidsElements and
|
|
||||||
# IfcRelProjectsElements.
|
|
||||||
if element.is_a() in ("IfcRelVoidsElement", "IfcRelProjectsElement") or not self.is_another_asset(
|
|
||||||
attribute
|
|
||||||
):
|
|
||||||
new_attribute = self.add_element(attribute)
|
new_attribute = self.add_element(attribute)
|
||||||
elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance):
|
elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance):
|
||||||
new_attribute = []
|
new_attribute = []
|
||||||
for item in attribute:
|
for item in attribute:
|
||||||
if self.is_another_asset(item):
|
if not is_dependent_rel and self.is_another_asset(item):
|
||||||
continue
|
continue
|
||||||
if skip_not_reused_entities_attr_i is not None and i == skip_not_reused_entities_attr_i:
|
if skip_not_reused_entities_attr_i is not None and i == skip_not_reused_entities_attr_i:
|
||||||
identity = item.wrapped_data.identity()
|
identity = item.wrapped_data.identity()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
import ifcopenshell.api.aggregate
|
||||||
import ifcopenshell.api.classification
|
import ifcopenshell.api.classification
|
||||||
import ifcopenshell.api.context
|
import ifcopenshell.api.context
|
||||||
import ifcopenshell.api.cost
|
import ifcopenshell.api.cost
|
||||||
@@ -409,6 +410,29 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3):
|
|||||||
ifcopenshell.api.project.append_asset(self.file, library=library, element=element)
|
ifcopenshell.api.project.append_asset(self.file, library=library, element=element)
|
||||||
assert self.file.by_type("IfcWall")[0].HasOpenings[0].RelatedOpeningElement.is_a("IfcOpeningElement")
|
assert self.file.by_type("IfcWall")[0].HasOpenings[0].RelatedOpeningElement.is_a("IfcOpeningElement")
|
||||||
|
|
||||||
|
def test_append_an_aggregate_with_its_parts_and_sub_aggregates_and_openings(self):
|
||||||
|
library = ifcopenshell.api.project.create_file(version=self.file.schema)
|
||||||
|
top = ifcopenshell.api.root.create_entity(library, ifc_class="IfcElementAssembly", name="House_Module")
|
||||||
|
sub_assembly = ifcopenshell.api.root.create_entity(
|
||||||
|
library, ifc_class="IfcElementAssembly", name="Wall_Panel_SubAssembly"
|
||||||
|
)
|
||||||
|
part = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWall", name="Panel Wall")
|
||||||
|
opening = ifcopenshell.api.root.create_entity(library, ifc_class="IfcOpeningElement")
|
||||||
|
ifcopenshell.api.feature.add_feature(library, feature=opening, element=part)
|
||||||
|
ifcopenshell.api.aggregate.assign_object(library, relating_object=sub_assembly, products=[part])
|
||||||
|
ifcopenshell.api.aggregate.assign_object(library, relating_object=top, products=[sub_assembly])
|
||||||
|
|
||||||
|
appended = ifcopenshell.api.project.append_asset(self.file, library=library, element=top)
|
||||||
|
|
||||||
|
assert len(self.file.by_type("IfcElementAssembly")) == 2
|
||||||
|
assert len(self.file.by_type("IfcWall")) == 1
|
||||||
|
assert len(self.file.by_type("IfcOpeningElement")) == 1
|
||||||
|
appended_sub = appended.IsDecomposedBy[0].RelatedObjects[0]
|
||||||
|
assert appended_sub.is_a("IfcElementAssembly")
|
||||||
|
appended_part = appended_sub.IsDecomposedBy[0].RelatedObjects[0]
|
||||||
|
assert appended_part.is_a("IfcWall")
|
||||||
|
assert appended_part.HasOpenings[0].RelatedOpeningElement.is_a("IfcOpeningElement")
|
||||||
|
|
||||||
def test_append_a_product_with_unrelated_relationships_to_openings(self):
|
def test_append_a_product_with_unrelated_relationships_to_openings(self):
|
||||||
library = ifcopenshell.api.project.create_file(version=self.file.schema)
|
library = ifcopenshell.api.project.create_file(version=self.file.schema)
|
||||||
ifcopenshell.api.root.create_entity(library, ifc_class="IfcProject")
|
ifcopenshell.api.root.create_entity(library, ifc_class="IfcProject")
|
||||||
|
|||||||
Reference in New Issue
Block a user