diff --git a/src/ifcblenderexport/blenderbim/bim/export_ifc.py b/src/ifcblenderexport/blenderbim/bim/export_ifc.py index a6e150bbb1..c836377840 100644 --- a/src/ifcblenderexport/blenderbim/bim/export_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/export_ifc.py @@ -471,39 +471,28 @@ class IfcParser: continue results[item_key] = {"ifc": None, "raw": raw, "material": material, "attributes": {"Name": item.name}} - def add_automatic_qtos(self, ifc_class, obj): + def add_automatic_qtos(self, ifc_class: str, obj): if not obj.data: return - qto_names = self.get_applicable_qtos(ifc_class) - for name in qto_names: - if name not in schema.ifc.psetqto.qtos: - continue + applicable_qtos = schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True) + for applicable_qto in applicable_qtos: has_automatic_value = False - props = schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys() guessed_values = {} - for prop_name in props: - value = self.qto_calculator.guess_quantity(prop_name, props, obj) + prop_names = [p.Name for p in applicable_qto.HasPropertyTemplates] + for prop_name in prop_names: + value = self.qto_calculator.guess_quantity(prop_name, prop_names, obj) if value: guessed_values[prop_name] = value has_automatic_value = True if has_automatic_value: qto = obj.BIMObjectProperties.qtos.add() - qto.name = name - for prop_name in props: + qto.name = applicable_qto.Name + for prop_name in prop_names: prop = qto.properties.add() prop.name = prop_name if prop_name in guessed_values: prop.string_value = str(guessed_values[prop_name]) - def get_applicable_qtos(self, ifc_class): - results = [] - empty = ifcopenshell.file(schema=self.ifc_export_settings.schema) - element = empty.create_entity(ifc_class) - for ifc_class, qto_names in schema.ifc.applicable_qtos.items(): - if element.is_a(ifc_class): - results.extend(qto_names) - return results - def get_product_relating_structure(self, product, obj): relating_structure = obj.BIMObjectProperties.relating_structure if relating_structure: @@ -1701,13 +1690,15 @@ class IfcExporter: pset["ifc"] = self.file.create_entity("IfcMaterialProperties", **pset["attributes"]) def create_qto_properties(self, qto): - if qto["attributes"]["Name"] in schema.ifc.psetqto.qtos: - return self.create_templated_qto_properties(qto) + qto_template = schema.ifc.psetqto.get_by_name(qto["attributes"]["Name"]) + if qto_template: + return self.create_templated_qto_properties(qto, qto_template) return self.create_custom_qto_properties(qto) def create_pset_properties(self, pset): - if pset["attributes"]["Name"] in schema.ifc.psetqto.psets: - return self.create_templated_pset_properties(pset) + pset_template = schema.ifc.psetqto.get_by_name(pset["attributes"]["Name"]) + if pset_template: + return self.create_templated_pset_properties(pset, pset_template) return self.create_custom_pset_properties(pset) def create_custom_pset_properties(self, pset): @@ -1737,15 +1728,15 @@ class IfcExporter: ) return properties - def create_templated_pset_properties(self, pset): + def create_templated_pset_properties(self, pset, pset_template): properties = [] - templates = schema.ifc.psetqto.psets[pset["attributes"]["Name"]]["HasPropertyTemplates"] - for name, data in templates.items(): + for prop in pset_template.HasPropertyTemplates: + name = prop.Name if name not in pset["raw"]: continue - if data.TemplateType == "P_SINGLEVALUE" or data.TemplateType == "P_ENUMERATEDVALUE": - if data.PrimaryMeasureType: - value_type = data.PrimaryMeasureType + if prop.TemplateType == "P_SINGLEVALUE" or prop.TemplateType == "P_ENUMERATEDVALUE": + if prop.PrimaryMeasureType: + value_type = prop.PrimaryMeasureType else: # The IFC spec is missing some, so we provide a fallback value_type = "IfcLabel" @@ -1755,7 +1746,8 @@ class IfcExporter: properties.append( self.file.create_entity("IfcPropertySingleValue", **{"Name": name, "NominalValue": nominal_value}) ) - invalid_pset_keys = [k for k in pset["raw"].keys() if k not in templates.keys()] + templates_names = [prop.Name for prop in qto_template.HasPropertyTemplates] + invalid_pset_keys = [k for k in pset["raw"].keys() if k not in templates_names] if invalid_pset_keys: self.ifc_export_settings.logger.error( "One or more properties were invalid in the pset {}: {}".format( @@ -1764,20 +1756,21 @@ class IfcExporter: ) return properties - def create_templated_qto_properties(self, qto): + def create_templated_qto_properties(self, qto, qto_template): properties = [] - templates = schema.ifc.psetqto.qtos[qto["attributes"]["Name"]]["HasPropertyTemplates"] - for name, data in templates.items(): + for prop in qto_template.HasPropertyTemplates: + name = prop.Name if name not in qto["raw"]: continue - if data.TemplateType[0:2] == "Q_": - value_basename = data.TemplateType[2:].title() + if prop.TemplateType[0:2] == "Q_": + value_basename = prop.TemplateType[2:].title() value_name = f"{value_basename}Value" class_name = f"IfcQuantity{value_basename}" properties.append( self.file.create_entity(class_name, **{"Name": name, value_name: float(qto["raw"][name])}) ) - invalid_qto_keys = [k for k in qto["raw"].keys() if k not in templates.keys()] + templates_names = [prop.Name for prop in qto_template.HasPropertyTemplates] + invalid_qto_keys = [k for k in qto["raw"].keys() if k not in templates_names] if invalid_qto_keys: self.ifc_export_settings.logger.error( "One or more properties were invalid in the qto {}/{}: {}".format( diff --git a/src/ifcblenderexport/blenderbim/bim/import_ifc.py b/src/ifcblenderexport/blenderbim/bim/import_ifc.py index e83cca9092..71f15756c1 100644 --- a/src/ifcblenderexport/blenderbim/bim/import_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/import_ifc.py @@ -1382,8 +1382,9 @@ class IfcImporter: def add_pset(self, pset, props): new_pset = props.psets.add() new_pset.name = pset.Name - if new_pset.name in schema.ifc.psetqto.psets: - for prop_name in schema.ifc.psetqto.psets[new_pset.name]["HasPropertyTemplates"].keys(): + pset_template = schema.ifc.psetqto.get_by_name(new_pset.name) + if pset_template: + for prop_name in (p.Name for p in pset_template.HasPropertyTemplates): prop = new_pset.properties.add() prop.name = prop_name try: @@ -1409,8 +1410,9 @@ class IfcImporter: def add_qto(self, qto, obj): new_qto = obj.BIMObjectProperties.qtos.add() new_qto.name = str(qto.Name) - if new_qto.name in schema.ifc.psetqto.qtos: - for prop_name in schema.ifc.psetqto.qtos[new_qto.name]["HasPropertyTemplates"].keys(): + qto_template = schema.ifc.psetqto.get_by_name(new_qto.name) + if qto_template: + for prop_name in (p.Name for p in qto_template.HasPropertyTemplates): prop = new_qto.properties.add() prop.name = prop_name for prop in qto.Quantities: diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index b3b62e95d8..f644796969 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -492,56 +492,46 @@ class AddQto(bpy.types.Operator): bl_label = "Add Qto" def execute(self, context): - self.applicable_qtos_cache = {} name = bpy.context.active_object.BIMObjectProperties.qto_name - if name not in schema.ifc.psetqto.qtos: + qto_template = schema.ifc.psetqto.get_by_name(name) + if not qto_template: return {"FINISHED"} for obj in bpy.context.selected_objects: if "/" not in obj.name or obj.BIMObjectProperties.qtos.find(name) != -1: continue - applicable_qtos = self.get_applicable_qtos(obj.name.split("/")[0]) + applicable_qtos = schema.ifc.psetqto.get_applicable_names(obj.name.split("/")[0], qto_only=True) if name not in applicable_qtos: continue qto = obj.BIMObjectProperties.qtos.add() qto.name = name - for prop_name in schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys(): + for prop_name in (p.Name for p in qto_template.HasPropertyTemplates): prop = qto.properties.add() prop.name = prop_name return {"FINISHED"} - def get_applicable_qtos(self, ifc_class): - if ifc_class not in self.applicable_qtos_cache: - self.applicable_qtos_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, qto_only=True) - return self.applicable_qtos_cache[ifc_class] - class AddPset(bpy.types.Operator): bl_idname = "bim.add_pset" bl_label = "Add Pset" def execute(self, context): - self.applicable_psets_cache = {} name = bpy.context.active_object.BIMObjectProperties.pset_name - if name not in schema.ifc.psetqto.psets: + pset_template = schema.ifc.psetqto.get_by_name(name) + if not pset_template: return {"FINISHED"} for obj in bpy.context.selected_objects: if "/" not in obj.name or obj.BIMObjectProperties.psets.find(name) != -1: continue - applicable_psets = self.get_applicable_psets(obj.name.split("/")[0]) + applicable_psets = schema.ifc.psetqto.get_applicable_names(obj.name.split("/")[0], pset_only=True) if name not in applicable_psets: continue pset = obj.BIMObjectProperties.psets.add() pset.name = name - for prop_name in schema.ifc.psetqto.psets[name]["HasPropertyTemplates"].keys(): + for prop_name in (p.Name for p in pset_template.HasPropertyTemplates): prop = pset.properties.add() prop.name = prop_name return {"FINISHED"} - def get_applicable_psets(self, ifc_class): - if ifc_class not in self.applicable_psets_cache: - self.applicable_psets_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, pset_only=True) - return self.applicable_psets_cache[ifc_class] - class RemovePset(bpy.types.Operator): bl_idname = "bim.remove_pset" @@ -582,13 +572,14 @@ class AddMaterialPset(bpy.types.Operator): def execute(self, context): material = bpy.context.active_object.active_material name = material.BIMMaterialProperties.pset_name - if name not in schema.ifc.psetqto.psets: + pset_template = schema.ifc.psetqto.get_by_name(name) + if not pset_template: return {"FINISHED"} if material.BIMMaterialProperties.psets.find(name) != -1: return {"FINISHED"} pset = material.BIMMaterialProperties.psets.add() pset.name = name - for prop_name in schema.ifc.psetqto.psets[name]["HasPropertyTemplates"].keys(): + for prop_name in (p.Name for p in pset_template.HasPropertyTemplates): prop = pset.properties.add() prop.name = prop_name return {"FINISHED"} @@ -2371,18 +2362,20 @@ class CopyPropertyToSelection(bpy.types.Operator): prop_value: bpy.props.StringProperty() def execute(self, context): - self.applicable_psets_cache = {} for obj in bpy.context.selected_objects: if "/" not in obj.name: continue pset = obj.BIMObjectProperties.psets.get(self.pset_name) if not pset: - applicable_psets = self.get_applicable_psets(obj.name.split("/")[0]) - if self.pset_name not in applicable_psets: + applicable_psets = schema.ifc.psetqto.get_applicable(obj.name.split("/")[0], pset_only=True) + for pset_template in applicable_psets: + if pset_template.Name == self.pset_name: + break + else: continue pset = obj.BIMObjectProperties.psets.add() pset.name = self.pset_name - for template_prop_name in schema.ifc.psetqto.psets[self.pset_name]["HasPropertyTemplates"].keys(): + for template_prop_name in (p.Name for p in pset_template.HasPropertyTemplates): prop = pset.properties.add() prop.name = template_prop_name prop = pset.properties.get(self.prop_name) @@ -2390,11 +2383,6 @@ class CopyPropertyToSelection(bpy.types.Operator): prop.string_value = self.prop_value return {"FINISHED"} - def get_applicable_psets(self, ifc_class): - if ifc_class not in self.applicable_psets_cache: - self.applicable_psets_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, pset_only=True) - return self.applicable_psets_cache[ifc_class] - class CopyAttributeToSelection(bpy.types.Operator): bl_idname = "bim.copy_attribute_to_selection" @@ -3193,11 +3181,12 @@ class GuessQuantity(bpy.types.Operator): prop.string_value = str(round(quantity, 3)) def add_qto(self, obj, name): - if name not in schema.ifc.psetqto.qtos: + qto_template = schema.ifc.psetqto.get_by_name(name) + if not qto_template: return qto = obj.BIMObjectProperties.qtos.add() qto.name = name - for prop_name in schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys(): + for prop_name in (p.Name for p in qto_template.HasPropertyTemplates): prop = qto.properties.add() prop.name = prop_name return qto diff --git a/src/ifcblenderexport/blenderbim/bim/schema.py b/src/ifcblenderexport/blenderbim/bim/schema.py index 1cc4d26453..338a03a2ea 100644 --- a/src/ifcblenderexport/blenderbim/bim/schema.py +++ b/src/ifcblenderexport/blenderbim/bim/schema.py @@ -30,7 +30,7 @@ class IfcSchema: self.property_files = [] property_paths = self.data_dir.joinpath("pset").glob("*.ifc") - self.psetqto = ifcopenshell.util.pset.PsetQto("IFC4", use_cache=True) + self.psetqto = ifcopenshell.util.pset.PsetQto("IFC4") for path in property_paths: self.psetqto.templates.append(ifcopenshell.open(path)) diff --git a/src/ifcopenshell-python/ifcopenshell/util/pset.py b/src/ifcopenshell-python/ifcopenshell/util/pset.py index d724287f51..6c59db8e6a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/pset.py +++ b/src/ifcopenshell-python/ifcopenshell/util/pset.py @@ -1,6 +1,7 @@ import pathlib import re -from typing import List, Generator +from functools import lru_cache +from typing import List, Generator, Optional import ifcopenshell from ifcopenshell.entity_instance import entity_instance @@ -11,34 +12,15 @@ class PsetQto: "IFC4": "Pset_IFC4_ADD2.ifc", } - def __init__(self, schema: str, templates=None, use_cache=False) -> None: + def __init__(self, schema: str, templates=None) -> None: self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema) if not templates: folder_path = pathlib.Path(__file__).parent.absolute() path = folder_path.joinpath("schema", self.templates_path[schema]) templates = [ifcopenshell.open(path)] self.templates = templates - # Caching reduce request time. For 100 get_applicable_names requests ~3.6 s -> ~2 s - self.use_cache = use_cache - self.psets = {} - self.qtos = {} - self.applicable_psets = {} - self.applicable_qtos = {} - if use_cache: - for template in templates: - self.cache_template(template) - - def cache_template(self, template): - for prop_set in template.by_type("IfcPropertySetTemplate"): - if prop_set.Name[0:4] == "Qto_": - self.qtos[prop_set.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop_set.HasPropertyTemplates}} - entity = prop_set.ApplicableEntity if prop_set.ApplicableEntity else "IfcRoot" - self.applicable_qtos.setdefault(entity, []).append(prop_set.Name) - else: - self.psets[prop_set.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop_set.HasPropertyTemplates}} - entity = prop_set.ApplicableEntity if prop_set.ApplicableEntity else "IfcRoot" - self.applicable_psets.setdefault(entity, []).append(prop_set.Name) + @lru_cache def get_applicable( self, ifc_class="", predefined_type="", pset_only=False, qto_only=False ) -> Generator[entity_instance, entity_instance, None]: @@ -58,22 +40,9 @@ class PsetQto: def get_applicable_names(self, ifc_class: str, predefined_type="", pset_only=False, qto_only=False) -> List[str]: """Return names instead of objects for other use eg. enum""" - if self.use_cache: - results = [] - entity = self.schema.declaration_by_name(ifc_class) - if not qto_only: - for applicable_class, pset_names in self.applicable_psets.items(): - if self.is_applicable(entity, applicable_class): - results.extend(pset_names) - if not pset_only: - for applicable_class, pset_names in self.applicable_qtos.items(): - if self.is_applicable(entity, applicable_class): - results.extend(pset_names) - return results - return [prop_set.Name for prop_set in self.get_applicable(ifc_class, predefined_type, pset_only, qto_only)] - def is_applicable(self, entity: entity_instance, applicables: str, predefined_type=""): + def is_applicable(self, entity: entity_instance, applicables: str, predefined_type="") -> bool: """applicables can have multiple possible patterns : IfcBoilerType (IfcClass) IfcBoilerType/STEAM (IfcClass/PREDEFINEDTYPE) @@ -95,3 +64,14 @@ class PsetQto: if entity.supertype(): return self.is_applicable(entity.supertype(), applicable_class) return False + + @lru_cache + def get_by_name(self, name: str) -> Optional[entity_instance]: + for template in self.templates: + for prop_set in template.by_type("IfcPropertySetTemplate"): + if prop_set.Name == name: + return prop_set + return None + + def is_templated(self, name: str) -> bool: + return bool(self.get_by_name(name))