diff --git a/src/ifcblenderexport/blenderbim/bim/export_ifc.py b/src/ifcblenderexport/blenderbim/bim/export_ifc.py index 935ce350e7..0e755ec314 100644 --- a/src/ifcblenderexport/blenderbim/bim/export_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/export_ifc.py @@ -8,7 +8,6 @@ import os import zipfile import tempfile import ifcopenshell -import ifcopenshell.util.pset import ifcopenshell.util.schema from pathlib import Path from mathutils import Vector, Matrix @@ -477,10 +476,10 @@ class IfcParser: return qto_names = self.get_applicable_qtos(ifc_class) for name in qto_names: - if name not in ifcopenshell.util.pset.qtos: + if name not in schema.ifc.psetqto.qtos: continue has_automatic_value = False - props = ifcopenshell.util.pset.qtos[name]["HasPropertyTemplates"].keys() + 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) @@ -1699,12 +1698,12 @@ class IfcExporter: pset["ifc"] = self.file.create_entity("IfcMaterialProperties", **pset["attributes"]) def create_qto_properties(self, qto): - if qto["attributes"]["Name"] in ifcopenshell.util.pset.qtos: + if qto["attributes"]["Name"] in schema.ifc.psetqto.qtos: return self.create_templated_qto_properties(qto) return self.create_custom_qto_properties(qto) def create_pset_properties(self, pset): - if pset["attributes"]["Name"] in ifcopenshell.util.pset.psets: + if pset["attributes"]["Name"] in schema.ifc.psetqto.psets: return self.create_templated_pset_properties(pset) return self.create_custom_pset_properties(pset) @@ -1737,7 +1736,7 @@ class IfcExporter: def create_templated_pset_properties(self, pset): properties = [] - templates = ifcopenshell.util.pset.psets[pset["attributes"]["Name"]]["HasPropertyTemplates"] + templates = schema.ifc.psetqto.psets[pset["attributes"]["Name"]]["HasPropertyTemplates"] for name, data in templates.items(): if name not in pset["raw"]: continue @@ -1764,7 +1763,7 @@ class IfcExporter: def create_templated_qto_properties(self, qto): properties = [] - templates = ifcopenshell.util.pset.qtos[qto["attributes"]["Name"]]["HasPropertyTemplates"] + templates = schema.ifc.psetqto.qtos[qto["attributes"]["Name"]]["HasPropertyTemplates"] for name, data in templates.items(): if name not in qto["raw"]: continue diff --git a/src/ifcblenderexport/blenderbim/bim/import_ifc.py b/src/ifcblenderexport/blenderbim/bim/import_ifc.py index f59a7401b3..bbe4fa7d41 100644 --- a/src/ifcblenderexport/blenderbim/bim/import_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/import_ifc.py @@ -3,7 +3,6 @@ import ifcopenshell.geom import ifcopenshell.util.geolocation import ifcopenshell.util.selector import ifcopenshell.util.element -import ifcopenshell.util.pset import bpy import bmesh import os @@ -22,6 +21,7 @@ from itertools import cycle from datetime import datetime from . import helper from . import ifc +from . import schema class FileCopy(threading.Thread): @@ -1394,8 +1394,8 @@ class IfcImporter: def add_pset(self, pset, props): new_pset = props.psets.add() new_pset.name = pset.Name - if new_pset.name in ifcopenshell.util.pset.psets: - for prop_name in ifcopenshell.util.pset.psets[new_pset.name]["HasPropertyTemplates"].keys(): + if new_pset.name in schema.ifc.psetqto.psets: + for prop_name in schema.ifc.psetqto.psets[new_pset.name]["HasPropertyTemplates"].keys(): prop = new_pset.properties.add() prop.name = prop_name try: @@ -1421,8 +1421,8 @@ 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 ifcopenshell.util.pset.qtos: - for prop_name in ifcopenshell.util.pset.qtos[new_qto.name]["HasPropertyTemplates"].keys(): + if new_qto.name in schema.ifc.psetqto.qtos: + for prop_name in schema.ifc.psetqto.qtos[new_qto.name]["HasPropertyTemplates"].keys(): 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 102b525102..a9ee97db61 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -10,7 +10,6 @@ import subprocess import ifcopenshell import ifcopenshell.util.selector import ifcopenshell.util.geolocation -import ifcopenshell.util.pset import ifcopenshell.util.schema import tempfile from . import export_ifc @@ -588,7 +587,7 @@ class AddQto(bpy.types.Operator): def execute(self, context): self.applicable_qtos_cache = {} name = bpy.context.active_object.BIMObjectProperties.qto_name - if name not in ifcopenshell.util.pset.qtos: + if name not in schema.ifc.psetqto.qtos: return {"FINISHED"} for obj in bpy.context.selected_objects: if "/" not in obj.name or obj.BIMObjectProperties.qtos.find(name) != -1: @@ -598,16 +597,14 @@ class AddQto(bpy.types.Operator): continue qto = obj.BIMObjectProperties.qtos.add() qto.name = name - for prop_name in ifcopenshell.util.pset.qtos[name]["HasPropertyTemplates"].keys(): + for prop_name in schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys(): 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] = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, ifc_class, is_qto=True - ) + self.applicable_qtos_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, qto_only=True) return self.applicable_qtos_cache[ifc_class] @@ -618,7 +615,7 @@ class AddPset(bpy.types.Operator): def execute(self, context): self.applicable_psets_cache = {} name = bpy.context.active_object.BIMObjectProperties.pset_name - if name not in ifcopenshell.util.pset.psets: + if name not in schema.ifc.psetqto.psets: return {"FINISHED"} for obj in bpy.context.selected_objects: if "/" not in obj.name or obj.BIMObjectProperties.psets.find(name) != -1: @@ -628,16 +625,14 @@ class AddPset(bpy.types.Operator): continue pset = obj.BIMObjectProperties.psets.add() pset.name = name - for prop_name in ifcopenshell.util.pset.psets[name]["HasPropertyTemplates"].keys(): + for prop_name in schema.ifc.psetqto.psets[name]["HasPropertyTemplates"].keys(): 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] = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, ifc_class, is_pset=True - ) + self.applicable_psets_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, pset_only=True) return self.applicable_psets_cache[ifc_class] @@ -680,13 +675,13 @@ 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 ifcopenshell.util.pset.psets: + if name not in schema.ifc.psetqto.psets: return {"FINISHED"} if material.BIMMaterialProperties.psets.find(name) != -1: return {"FINISHED"} pset = material.BIMMaterialProperties.psets.add() pset.name = name - for prop_name in ifcopenshell.util.pset.psets[name]["HasPropertyTemplates"].keys(): + for prop_name in schema.ifc.psetqto.psets[name]["HasPropertyTemplates"].keys(): prop = pset.properties.add() prop.name = prop_name return {"FINISHED"} @@ -2649,7 +2644,7 @@ class CopyPropertyToSelection(bpy.types.Operator): continue pset = obj.BIMObjectProperties.psets.add() pset.name = self.pset_name - for template_prop_name in ifcopenshell.util.pset.psets[self.pset_name]["HasPropertyTemplates"].keys(): + for template_prop_name in schema.ifc.psetqto.psets[self.pset_name]["HasPropertyTemplates"].keys(): prop = pset.properties.add() prop.name = template_prop_name prop = pset.properties.get(self.prop_name) @@ -2659,9 +2654,7 @@ class CopyPropertyToSelection(bpy.types.Operator): def get_applicable_psets(self, ifc_class): if ifc_class not in self.applicable_psets_cache: - self.applicable_psets_cache[ifc_class] = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, ifc_class, is_pset=True - ) + self.applicable_psets_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, pset_only=True) return self.applicable_psets_cache[ifc_class] @@ -3573,11 +3566,11 @@ class GuessQuantity(bpy.types.Operator): prop.string_value = str(round(quantity, 3)) def add_qto(self, obj, name): - if name not in ifcopenshell.util.pset.qtos: + if name not in schema.ifc.psetqto.qtos: return qto = obj.BIMObjectProperties.qtos.add() qto.name = name - for prop_name in ifcopenshell.util.pset.qtos[name]["HasPropertyTemplates"].keys(): + for prop_name in schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys(): prop = qto.properties.add() prop.name = prop_name return qto diff --git a/src/ifcblenderexport/blenderbim/bim/prop.py b/src/ifcblenderexport/blenderbim/bim/prop.py index b80e6a159d..5da7176506 100644 --- a/src/ifcblenderexport/blenderbim/bim/prop.py +++ b/src/ifcblenderexport/blenderbim/bim/prop.py @@ -1,7 +1,6 @@ import json import os import ifcopenshell -import ifcopenshell.util.pset from pathlib import Path from . import export_ifc from . import schema @@ -433,9 +432,7 @@ def getPsetNames(self, context): global psetnames_enum psetnames_enum.clear() if "/" in context.active_object.name and context.active_object.name.split("/")[0] in schema.ifc.elements: - pset_names = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, context.active_object.name.split("/")[0], is_pset=True - ) + pset_names = schema.ifc.psetqto.get_applicable_names(context.active_object.name.split("/")[0], pset_only=True) psetnames_enum.extend([(p, p, "") for p in pset_names]) return psetnames_enum @@ -443,9 +440,7 @@ def getPsetNames(self, context): def getMaterialPsetNames(self, context): global materialpsetnames_enum materialpsetnames_enum.clear() - pset_names = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, "IfcMaterial", is_pset=True - ) + pset_names = schema.ifc.psetqto.get_applicable_names("IfcMaterial", pset_only=True) materialpsetnames_enum.extend([(p, p, "") for p in pset_names]) return materialpsetnames_enum @@ -454,9 +449,7 @@ def getQtoNames(self, context): global qtonames_enum qtonames_enum.clear() if "/" in context.active_object.name and context.active_object.name.split("/")[0] in schema.ifc.elements: - qto_names = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, context.active_object.name.split("/")[0], is_qto=True - ) + qto_names = schema.ifc.psetqto.get_applicable_names(context.active_object.name.split("/")[0], qto_only=True) qtonames_enum.extend([(q, q, "") for q in qto_names]) return qtonames_enum diff --git a/src/ifcblenderexport/blenderbim/bim/schema.py b/src/ifcblenderexport/blenderbim/bim/schema.py index 958567ac09..1cc4d26453 100644 --- a/src/ifcblenderexport/blenderbim/bim/schema.py +++ b/src/ifcblenderexport/blenderbim/bim/schema.py @@ -10,8 +10,8 @@ cwd = os.path.dirname(os.path.realpath(__file__)) class IfcSchema: def __init__(self): - self.schema_dir = os.path.join(cwd, "schema") # TODO: make configurable - self.data_dir = os.path.join(cwd, "data") # TODO: make configurable + self.schema_dir = Path(cwd).joinpath("schema") # TODO: make configurable + self.data_dir = Path(cwd).joinpath("data") # TODO: make configurable # TODO: Make it less troublesome self.products = [ "IfcContext", @@ -29,10 +29,10 @@ class IfcSchema: self.elements = {} self.property_files = [] - property_paths = Path(os.path.join(self.data_dir, "pset")).glob("*.ifc") + property_paths = self.data_dir.joinpath("pset").glob("*.ifc") + self.psetqto = ifcopenshell.util.pset.PsetQto("IFC4", use_cache=True) for path in property_paths: - ifcopenshell.util.pset.load_property_set_template(path) - ifcopenshell.util.pset.load_property_set_template(os.path.join(self.schema_dir, "Pset_IFC4_ADD2.ifc")) + self.psetqto.templates.append(ifcopenshell.open(path)) self.classification_files = {} self.classifications = {} diff --git a/src/ifcopenshell-python/ifcopenshell/util/pset.py b/src/ifcopenshell-python/ifcopenshell/util/pset.py index 5e5a541528..d724287f51 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/pset.py +++ b/src/ifcopenshell-python/ifcopenshell/util/pset.py @@ -1,39 +1,97 @@ +import pathlib +import re +from typing import List, Generator + import ifcopenshell - -property_set_template_files = [] -psets = {} -qtos = {} -applicable_psets = {} -applicable_qtos = {} +from ifcopenshell.entity_instance import entity_instance -def load_property_set_template(path): - property_set_template_files.append(ifcopenshell.open(path)) - for prop in property_set_template_files[-1].by_type("IfcPropertySetTemplate"): - if prop.Name[0:4] == "Qto_": - qtos[prop.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop.HasPropertyTemplates}} - entity = prop.ApplicableEntity if prop.ApplicableEntity else "IfcRoot" - applicable_qtos.setdefault(entity, []).append(prop.Name) - else: - psets[prop.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop.HasPropertyTemplates}} - entity = prop.ApplicableEntity if prop.ApplicableEntity else "IfcRoot" - applicable_psets.setdefault(entity, []).append(prop.Name) +class PsetQto: + templates_path = { + "IFC4": "Pset_IFC4_ADD2.ifc", + } + def __init__(self, schema: str, templates=None, use_cache=False) -> 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 get_applicable_psetqtos(schema_version, ifc_class, is_pset=False, is_qto=False): - def is_a(entity, ifc_class): - if entity.name() == ifc_class: - return True - return is_a(entity.supertype(), ifc_class) if entity.supertype() else False + 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) - results = [] - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_version) - entity = schema.declaration_by_name(ifc_class) - if is_pset: - search_items = applicable_psets.items() - elif is_qto: - search_items = applicable_qtos.items() - for ifc_class, pset_names in search_items: - if is_a(entity, ifc_class): - results.extend(pset_names) - return results + def get_applicable( + self, ifc_class="", predefined_type="", pset_only=False, qto_only=False + ) -> Generator[entity_instance, entity_instance, None]: + any_class = not ifc_class + if not any_class: + entity = self.schema.declaration_by_name(ifc_class) + for template in self.templates: + for prop_set in template.by_type("IfcPropertySetTemplate"): + if pset_only: + if prop_set.Name.startswith("Qto_"): + continue + if qto_only: + if not prop_set.Name.startswith("Qto_"): + continue + if any_class or self.is_applicable(entity, prop_set.ApplicableEntity or "IfcRoot", predefined_type): + yield prop_set + + 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=""): + """applicables can have multiple possible patterns : + IfcBoilerType (IfcClass) + IfcBoilerType/STEAM (IfcClass/PREDEFINEDTYPE) + IfcBoilerType[PerformanceHistory] (IfcClass[PerformanceHistory]) + IfcBoilerType/STEAM[PerformanceHistory] (IfcClass/PREDEFINEDTYPE[PerformanceHistory]) + """ + for applicable in applicables.split(","): + match = re.match(r"(\w+)(\[\w+\])*/*(\w+)*(\[\w+\])*", applicable) + if not match: + continue + # Uncomment if usage found + # applicable_perf_history = match.group(2) or match.group(4) + if predefined_type and predefined_type != match.group(3): + continue + + applicable_class = match.group(1) + if entity.name() == applicable_class: + return True + if entity.supertype(): + return self.is_applicable(entity.supertype(), applicable_class) + return False diff --git a/src/ifcblenderexport/blenderbim/bim/schema/Pset_IFC4_ADD2.ifc b/src/ifcopenshell-python/ifcopenshell/util/schema/Pset_IFC4_ADD2.ifc similarity index 100% rename from src/ifcblenderexport/blenderbim/bim/schema/Pset_IFC4_ADD2.ifc rename to src/ifcopenshell-python/ifcopenshell/util/schema/Pset_IFC4_ADD2.ifc diff --git a/src/ifcwrap/CMakeLists.txt b/src/ifcwrap/CMakeLists.txt index 86ad0c1ace..0e8a695656 100644 --- a/src/ifcwrap/CMakeLists.txt +++ b/src/ifcwrap/CMakeLists.txt @@ -84,8 +84,7 @@ IF(PYTHONINTERP_FOUND AND NOT "${PYTHON_EXECUTABLE}" STREQUAL "") MESSAGE(WARNING "Unable to locate Python site-package directory, unable to install the Python wrapper") ELSE() FILE(GLOB_RECURSE sourcefiles - "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*.py" - "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*.bnf" + "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*" ) FOREACH(file ${sourcefiles}) FILE(RELATIVE_PATH relative "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/" "${file}")