From eb9591e9a2c155c3e89ed08b45fc7c74925d005f Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sat, 11 Jul 2026 09:13:38 +0300 Subject: [PATCH] Support appending IfcGroup assets via project.append_asset (#8162) append_asset could not import an IfcGroup from a project library: IfcGroup was absent from APPENDABLE_ASSET, so execute() fell through every branch and returned None, appending nothing. Add IfcGroup to APPENDABLE_ASSET and an append_group() routine that mirrors append_product(): it whitelists the group's IsGroupedBy inverse so the members reached through IfcRelAssignsToGroup are copied via the regular element-append path (bringing their materials, properties and representations), and the relationship is recreated in the destination linking the copied group to the copied members. Idempotency comes from the existing GUID-based reuse, so re-appending the same group does not duplicate it. Verified headless: appending a group of three walls yields group=1, walls=3, IfcRelAssignsToGroup=1 with RelatingGroup and RelatedObjects correctly wired; re-appending is a no-op; appending a plain IfcProduct still works unchanged. Co-Authored-By: Claude Opus 4.8 --- .../ifcopenshell/api/project/append_asset.py | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 8a8f2307a4..99ce251ce9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -39,6 +39,7 @@ APPENDABLE_ASSET = Literal[ "IfcCostSchedule", "IfcProfileDef", "IfcPresentationStyle", + "IfcGroup", ] APPENDABLE_ASSET_TYPES: tuple[APPENDABLE_ASSET, ...] = get_args(APPENDABLE_ASSET) MATERIAL_SETS = ("IfcMaterialLayerSet", "IfcMaterialConstituentSet", "IfcMaterialProfileSet") @@ -68,8 +69,8 @@ def append_asset( :param library: The file object containing the asset. :param element: An element in the library file of the asset. It may be - an IfcTypeProduct, IfcProduct, IfcMaterial, IfcCostSchedule, or - IfcProfileDef. + an IfcTypeProduct, IfcProduct, IfcMaterial, IfcCostSchedule, + IfcProfileDef, or IfcGroup. :param reuse_identities: Optional dictionary of mapped entities' identities to the already created elements. It will be used to avoid creating duplicated inverse elements during multiple `project.append_asset` calls. If you want @@ -236,6 +237,9 @@ class Usecase: elif self.settings["element"].is_a("IfcPresentationStyle"): self.target_class = "IfcPresentationStyle" return self.append_presentation_style() + elif self.settings["element"].is_a("IfcGroup"): + self.target_class = "IfcGroup" + return self.append_group() def by_guid(self, guid: str) -> Union[ifcopenshell.entity_instance, None]: try: @@ -393,6 +397,33 @@ class Usecase: self.reuse_existing_contexts() return element + def append_group(self): + # A group (IfcGroup and subtypes like IfcSystem, IfcZone) is appended + # together with its members. Members are reached through the group's + # ``IsGroupedBy`` inverse (the IfcRelAssignsToGroup relationship), whose + # ``RelatedObjects`` are copied via the regular element-append path, so + # they bring their own materials, properties, and representations. The + # relationship itself is recreated in the destination linking the copied + # group to the copied members. + self.whitelisted_inverse_attributes = { + "IfcObjectDefinition": ["HasAssociations"], + "IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"], + "IfcGroup": ["IsGroupedBy"], + "IfcElement": ["HasOpenings"], + self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"], + "IfcRepresentationItem": [ + "StyledByItem", + "LayerAssignments" if self.file.schema == "IFC2X3" else "LayerAssignment", + ], + "IfcRepresentation": ["LayerAssignments"], + "IfcProductDefinitionShape": ["HasShapeAspects"], + "IfcRepresentationMap": ["HasShapeAspects"], + } + self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext") + element = self.add_element(self.settings["element"]) + self.reuse_existing_contexts() + return element + def append_product(self): self.whitelisted_inverse_attributes = { "IfcObjectDefinition": ["HasAssociations"],