Do not deduplicate appended assets by a blank name (#8045)

project.append_asset reuses an existing profile, material, material set or
presentation style in the destination when it shares the appended asset's
Name. An empty or blank string was treated as a real key, so two distinct
assets that both have an empty name (e.g. two different
IfcArbitraryClosedProfileDef named "") collapsed into one on append (and
via the ExtractElements recipe). IfcMaterial had no guard at all.

Add is_reusable_name(), which accepts only a non-empty, non-blank string,
and use it for the name-based dedup of IfcProfileDef, IfcMaterial, the
material sets, and IfcPresentationStyle in both get_existing_element and
file_add. Blank/None-named assets are now appended as distinct instances;
non-empty names still dedup exactly as before, and rooted GUID-based reuse
is untouched.

Verified headless: two distinct blank-named profiles now yield two in the
destination (was one), a real-named pair still dedups to one, and
re-appending a named asset stays idempotent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Petru Conduraru
2026-07-11 10:52:34 +03:00
parent 256d5a63f1
commit daa5800e74
@@ -44,6 +44,18 @@ APPENDABLE_ASSET_TYPES: tuple[APPENDABLE_ASSET, ...] = get_args(APPENDABLE_ASSET
MATERIAL_SETS = ("IfcMaterialLayerSet", "IfcMaterialConstituentSet", "IfcMaterialProfileSet")
def is_reusable_name(name: Any) -> bool:
"""Whether a name qualifies as a key for name-based deduplication.
Only a non-empty, non-blank string identifies an asset well enough to
reuse an existing one. An empty/blank string or ``None`` is not a real
name, so distinct blank-named assets (e.g. two different profiles both
named ``""``) must be appended as separate instances instead of collapsed
into one. See https://github.com/IfcOpenShell/IfcOpenShell/issues/8045.
"""
return isinstance(name, str) and bool(name.strip())
def append_asset(
file: ifcopenshell.file,
library: ifcopenshell.file,
@@ -315,14 +327,16 @@ class Usecase:
return None
elif element.is_a("IfcMaterial"):
name = element.Name
if not is_reusable_name(name):
return None
return next((e for e in self.file.by_type("IfcMaterial") if e.Name == name), None)
elif element.is_a() in MATERIAL_SETS:
ifc_class = element.is_a()
name_attr = "LayerSetName" if ifc_class == "IfcMaterialLayerSet" else "Name"
material_set_name = getattr(element, name_attr)
if material_set_name is None:
return
if not is_reusable_name(material_set_name):
return None
for candidate in self.file.by_type(ifc_class):
if getattr(candidate, name_attr) == material_set_name:
if self.material_sets_are_equal(element, candidate):
@@ -331,12 +345,12 @@ class Usecase:
elif element.is_a("IfcProfileDef"):
profile_name = element.ProfileName
if profile_name is None:
if not is_reusable_name(profile_name):
return None
return next((e for e in self.file.by_type("IfcProfileDef") if e.ProfileName == profile_name), None)
elif element.is_a("IfcPresentationStyle"):
name = element.Name
if name is None:
if not is_reusable_name(name):
return None
return next((e for e in self.file.by_type(element.is_a()) if e.Name == name), None)
@@ -707,7 +721,7 @@ class Usecase:
# then it might create duplicated subelements.
if element.is_a("IfcProfileDef"):
profile_name = element.ProfileName
if profile_name is not None:
if is_reusable_name(profile_name):
existing_profile = next(
(e for e in ifc_file.by_type("IfcProfileDef") if e.ProfileName == profile_name), None
)
@@ -717,15 +731,16 @@ class Usecase:
elif element.is_a("IfcMaterial"):
material_name = element.Name
existing_material = next((e for e in ifc_file.by_type("IfcMaterial") if e.Name == material_name), None)
if existing_material is not None:
reuse_identities[element_identity] = existing_material
return existing_material
if is_reusable_name(material_name):
existing_material = next((e for e in ifc_file.by_type("IfcMaterial") if e.Name == material_name), None)
if existing_material is not None:
reuse_identities[element_identity] = existing_material
return existing_material
elif ifc_class in MATERIAL_SETS:
name_attr = "LayerSetName" if ifc_class == "IfcMaterialLayerSet" else "Name"
material_set_name = getattr(element, name_attr)
if material_set_name is not None:
if is_reusable_name(material_set_name):
for candidate in ifc_file.by_type(ifc_class):
if getattr(candidate, name_attr) == material_set_name:
if self.material_sets_are_equal(element, candidate):
@@ -734,7 +749,7 @@ class Usecase:
elif element.is_a("IfcPresentationStyle"):
style_name = element.Name
if style_name is not None:
if is_reusable_name(style_name):
existing_style = next((e for e in ifc_file.by_type(ifc_class) if e.Name == style_name), None)
if existing_style is not None:
reuse_identities[element_identity] = existing_style