diff --git a/src/blenderbim/blenderbim/bim/module/material/__init__.py b/src/blenderbim/blenderbim/bim/module/material/__init__.py index 7680e62122..83ef17c184 100644 --- a/src/blenderbim/blenderbim/bim/module/material/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/material/__init__.py @@ -10,6 +10,7 @@ classes = ( operator.RemoveConstituent, operator.AddProfile, operator.RemoveProfile, + operator.AssignParameterizedProfile, operator.AddLayer, operator.RemoveLayer, operator.ReorderMaterialSetItem, diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index c77e96048b..b5ff702d02 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -1,8 +1,36 @@ import bpy +import json import ifcopenshell.api import ifcopenshell.util.attribute from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.material.data import Data +from ifcopenshell.api.profile.data import Data as ProfileData + + +class AssignParameterizedProfile(bpy.types.Operator): + bl_idname = "bim.assign_parameterized_profile" + bl_label = "Assign Parameterized Profile" + ifc_class: bpy.props.StringProperty() + material_profile: bpy.props.IntProperty() + obj: bpy.props.StringProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + self.file = IfcStore.get_file() + profile = ifcopenshell.api.run( + "profile.add_parameterized_profile", + self.file, + **{"ifc_class": self.ifc_class}, + ) + ifcopenshell.api.run( + "material.assign_profile", + self.file, + **{"material_profile": self.file.by_id(self.material_profile), "profile": profile} + ) + Data.load_profiles() + ProfileData.load(self.file) + bpy.ops.bim.enable_editing_material_set_item(obj=obj.name, material_set_item=self.material_profile) + return {"FINISHED"} class AddMaterial(bpy.types.Operator): @@ -370,8 +398,8 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object - props = obj.BIMObjectMaterialProperties - props.active_material_set_item_id = self.material_set_item + self.props = obj.BIMObjectMaterialProperties + self.props.active_material_set_item_id = self.material_set_item product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] material_set_item = self.file.by_id(self.material_set_item) @@ -384,17 +412,24 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): else: material_set_item_data = {} - props.material_set_item_material = str(material_set_item_data["Material"]) + self.props.material_set_item_material = str(material_set_item_data["Material"]) - while len(props.material_set_item_attributes) > 0: - props.material_set_item_attributes.remove(0) + self.load_set_item_attributes(material_set_item, material_set_item_data) + if material_set_item.is_a("IfcMaterialProfile"): + self.load_profile_attributes(material_set_item, material_set_item_data) + + return {"FINISHED"} + + def load_set_item_attributes(self, material_set_item, material_set_item_data): + while len(self.props.material_set_item_attributes) > 0: + self.props.material_set_item_attributes.remove(0) for attribute in IfcStore.get_schema().declaration_by_name(material_set_item.is_a()).all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue if attribute.name() in material_set_item_data: - new = props.material_set_item_attributes.add() + new = self.props.material_set_item_attributes.add() new.name = attribute.name() new.is_null = material_set_item_data[attribute.name()] is None new.data_type = data_type @@ -406,7 +441,45 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): new.int_value = 0 if new.is_null else material_set_item_data[attribute.name()] elif data_type == "boolean": new.bool_value = False if new.is_null else material_set_item_data[attribute.name()] - return {"FINISHED"} + + def load_profile_attributes(self, material_set_item, material_set_item_data): + while len(self.props.material_set_item_profile_attributes) > 0: + self.props.material_set_item_profile_attributes.remove(0) + + if not material_set_item_data["Profile"]: + return + + profile = self.file.by_id(material_set_item_data["Profile"]) + profile_data = ProfileData.profiles[material_set_item_data["Profile"]] + + for attribute in IfcStore.get_schema().declaration_by_name(profile.is_a()).all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity": + continue + if attribute.name() in profile_data: + new = self.props.material_set_item_profile_attributes.add() + new.name = attribute.name() + new.is_null = profile_data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if data_type == "string": + new.string_value = "" if new.is_null else profile_data[attribute.name()] + elif data_type == "float": + new.float_value = 0.0 if new.is_null else profile_data[attribute.name()] + elif data_type == "integer": + new.int_value = 0 if new.is_null else profile_data[attribute.name()] + elif data_type == "boolean": + new.bool_value = False if new.is_null else profile_data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if profile_data[attribute.name()]: + new.enum_value = profile_data[attribute.name()] + + # Force null to be false if the attribute is mandatory because when we first assign a profile, all of + # its fields are null (which is illegal). + # TODO: find a better solution. + if not new.is_optional: + new.is_null = False class DisableEditingMaterialSetItem(bpy.types.Operator): @@ -468,16 +541,31 @@ class EditMaterialSetItem(bpy.types.Operator): ) Data.load_layers() elif product_data["type"] == "IfcMaterialProfileSet": + profile_attributes = {} + for attribute in props.material_set_item_profile_attributes: + if attribute.data_type == "string": + value = attribute.string_value + elif attribute.data_type == "float": + value = attribute.float_value + elif attribute.data_type == "integer": + value = attribute.int_value + elif attribute.data_type == "boolean": + value = attribute.bool_value + elif attribute.data_type == "enum": + value = attribute.enum_value + profile_attributes[attribute.name] = None if attribute.is_null else value ifcopenshell.api.run( "material.edit_profile", self.file, **{ "profile": self.file.by_id(self.material_set_item), "attributes": attributes, + "profile_attributes": profile_attributes, "material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)), }, ) Data.load_profiles() + ProfileData.load(self.file) else: pass diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index 4795832bbc..ffa7c5d87a 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -1,5 +1,5 @@ import bpy -import blenderbim.bim.schema # refactor +import blenderbim.bim.schema # refactor from ifcopenshell.api.material.data import Data from blenderbim.bim.ifc import IfcStore from blenderbim.bim.prop import StrProperty, Attribute @@ -17,6 +17,29 @@ from bpy.props import ( materials_enum = [] materialtypes_enum = [] +profileclasses_enum = [] +parameterizedprofileclasses_enum = [] + + +def getProfileClasses(self, context): + global profileclasses_enum + if len(profileclasses_enum) == 0 and IfcStore.get_schema(): + profileclasses_enum.clear() + profileclasses_enum = [ + (t.name(), t.name(), "") for t in IfcStore.get_schema().declaration_by_name("IfcProfileDef").subtypes() + ] + return profileclasses_enum + + +def getParameterizedProfileClasses(self, context): + global parameterizedprofileclasses_enum + if len(parameterizedprofileclasses_enum) == 0 and IfcStore.get_schema(): + parameterizedprofileclasses_enum.clear() + parameterizedprofileclasses_enum = [ + (t.name(), t.name(), "") + for t in IfcStore.get_schema().declaration_by_name("IfcParameterizedProfileDef").subtypes() + ] + return parameterizedprofileclasses_enum def getMaterials(self, context): @@ -52,4 +75,9 @@ class BIMObjectMaterialProperties(PropertyGroup): material_set_attributes: CollectionProperty(name="Material Set Attributes", type=Attribute) active_material_set_item_id: IntProperty(name="Active Material Set ID") material_set_item_attributes: CollectionProperty(name="Material Set Item Attributes", type=Attribute) + material_set_item_profile_attributes: CollectionProperty(name="Material Set Item Profile Attributes", type=Attribute) material_set_item_material: EnumProperty(items=getMaterials, name="Material") + profile_classes: EnumProperty(items=getProfileClasses, name="Profile Classes") + parameterized_profile_classes: EnumProperty( + items=getParameterizedProfileClasses, name="Parameterized Profile Classes" + ) diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 6d432f673a..591fa8ccb3 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -1,5 +1,6 @@ from bpy.types import Panel from ifcopenshell.api.material.data import Data +from ifcopenshell.api.profile.data import Data as ProfileData from blenderbim.bim.ifc import IfcStore @@ -46,6 +47,8 @@ class BIM_PT_object_material(Panel): Data.load(IfcStore.get_file()) if self.oprops.ifc_definition_id not in Data.products: Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id) + if not ProfileData.is_loaded: + ProfileData.load(self.file) self.product_data = Data.products[self.oprops.ifc_definition_id] if not Data.materials: @@ -172,6 +175,39 @@ class BIM_PT_object_material(Panel): row.prop(attribute, "bool_value", text=attribute.name) row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + if self.set_item_name == "profile": + self.draw_assign_profile_ui(box, item) + self.draw_editable_profile_ui(box, item) + + def draw_assign_profile_ui(self, layout, item): + row = layout.row(align=True) + row.prop(self.props, "profile_classes", text="") + if self.props.profile_classes == "IfcParameterizedProfileDef": + row.prop(self.props, "parameterized_profile_classes", text="") + op = row.operator("bim.assign_parameterized_profile", icon="GREASEPENCIL" if item["Profile"] else "ADD", text="") + op.ifc_class = self.props.parameterized_profile_classes + op.material_profile = item["id"] + else: + # TODO: support non parametric profiles by showing a list of named profiles to select from, or an + # eyedropper to pick profile geometry from the scene + row.operator("bim.disable_editing_material_set_item", icon="X", text="") + + def draw_editable_profile_ui(self, layout, item): + for attribute in self.props.material_set_item_profile_attributes: + row = layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "float": + row.prop(attribute, "float_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False): if self.product_data["type"] == "IfcMaterialList": item = Data.materials[set_item_id] diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py new file mode 100644 index 0000000000..43580552c4 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -0,0 +1,14 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"material_profile": None, "profile": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + if ( + self.settings["material_profile"].Profile + and len(self.file.get_inverse(self.settings["material_profile"].Profile)) == 1 + ): + self.file.remove(self.settings["material_profile"].Profile) + self.settings["material_profile"].Profile = self.settings["profile"] diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py index f833922d30..5ab716644b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py @@ -4,6 +4,7 @@ class Usecase(): self.settings = { "profile": None, "attributes": {}, + "profile_attributes": {}, "material": None } for key, value in settings.items(): @@ -13,3 +14,5 @@ class Usecase(): for name, value in self.settings["attributes"].items(): setattr(self.settings["profile"], name, value) self.settings["profile"].Material = self.settings["material"] + for name, value in self.settings["profile_attributes"].items(): + setattr(self.settings["profile"].Profile, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py new file mode 100644 index 0000000000..39abc879e8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py @@ -0,0 +1,9 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"ifc_class": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + return self.file.create_entity(self.settings["ifc_class"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/data.py b/src/ifcopenshell-python/ifcopenshell/api/profile/data.py new file mode 100644 index 0000000000..99b96a13e9 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/data.py @@ -0,0 +1,17 @@ +class Data: + is_loaded = False + profiles = {} + + @classmethod + def purge(cls): + cls.is_loaded = False + cls.profiles = {} + + @classmethod + def load(cls, file): + if not file: + return + cls.profiles = {} + for profile in file.by_type("IfcProfileDef"): + cls.profiles[profile.id()] = profile.get_info() + cls.is_loaded = True