diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index e334be936b..b5282e63ea 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -18,6 +18,7 @@ import bpy import bmesh +import idprop import ifcopenshell import ifcopenshell.api import ifcopenshell.api.geometry @@ -180,12 +181,16 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_class" bl_label = "Assign IFC Class" bl_options = {"REGISTER", "UNDO"} - bl_description = "Assign the IFC Class to the selected non-ifc objects." + bl_description = ( + "Assign the IFC Class to the selected non-ifc objects.\n\n" + + "ALT+CLICK to also convert object's custom properties to custom Pset." + ) obj: bpy.props.StringProperty() ifc_class: bpy.props.StringProperty() predefined_type: bpy.props.StringProperty() userdefined_type: bpy.props.StringProperty() context_id: bpy.props.IntProperty() + props_to_pset: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # TODO: is never used? should_add_representation: bpy.props.BoolProperty(default=True) @@ -212,6 +217,10 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): return False return True + def invoke(self, context, event): + self.props_to_pset = event.alt + return self.execute(context) + def _execute(self, context): ifc_file = tool.Ifc.get() props = tool.Root.get_root_props() @@ -360,6 +369,19 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): elif obj.data is not None: new_obj = tool.Geometry.recreate_object_with_data(obj, None) + # Accomodate existing importers to Blender from other formats that set custom props + if self.props_to_pset: + custom_props = {} + for k, v in obj.items(): + if type(v) in [bool, int, float, str]: + custom_props[k] = v + elif type(v) is idprop.types.IDPropertyArray: + for idx in range(len(v)): + custom_props["{}.{}".format(k, idx + 1)] = v[idx] + + pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_ImportedBlenderProps") + ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties=custom_props) + # TODO: reload representation might lead to the object being replaced by object of the other type. # We probably should track it somehow and keep the original selection. diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py index 73031d1d36..6928083c8d 100644 --- a/src/bonsai/test/tool/test_root.py +++ b/src/bonsai/test/tool/test_root.py @@ -238,3 +238,77 @@ class TestReassignClass(NewFile): assert len(ifc_file.by_type("IfcSlab")) == 0 assert len(ifc_file.by_type("IfcWallType")) == n_wall_types + 1 assert len(ifc_file.by_type("IfcSlabType")) == n_slab_types - 1 + + +class TestAssignClass(NewFile): + def create_objects(self, context): + # Create blender cylinder + bpy.ops.mesh.primitive_cylinder_add(vertices=10, location=(0, 4, 0)) + datablock_obj = bpy.data.objects["Cylinder"] + + # Create blender cube w/ props + bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0)) + obj = bpy.data.objects["Cube"] + # Set all the custom properties on the obj + obj["01_float"] = 3.14159 + obj["02_float_array"] = [3.14159, 1.61803, 2.71828] + obj["03_integer"] = 2 + obj["04_integer_array"] = [2, 3, 5, 7] + obj["05_boolean"] = True + obj["06_boolean_array"] = [True, False] + obj["07_string"] = "Bonsai!" + # Data Block is not proper Pointer in UI. Probably doesn't matter. + obj["08_data_block"] = datablock_obj + # Python expressions can also be stored, i.e. dictionary + obj["09_python"] = {"test": 12} + # Or a list of things, i.e. objects: [bpy.data.objects['IfcBuildingElementProxy/Cube']] + obj["10_python"] = bpy.context.selected_objects + + return obj, datablock_obj + + def test_normal_assign_ifc_class(self): + # Setup project + tool.Project.get_project_props().template_file = "IFC4 Demo Template.ifc" + bpy.ops.bim.create_project() + ifc_file = tool.Ifc.get() + context = bpy.context + obj, datablock_obj = self.create_objects(context) + + # Assign IfcClass + bpy.ops.bim.assign_class(ifc_class="IfcBuildingElementProxy", predefined_type="ELEMENT", userdefined_type="") + element = tool.Ifc.get_entity(obj) + assert element + + # Get Psets + psets = ifcopenshell.util.element.get_psets(element, psets_only=True) + assert psets == {} + + def test_alternative_assign_ifc_class(self): + # Setup project + tool.Project.get_project_props().template_file = "IFC4 Demo Template.ifc" + bpy.ops.bim.create_project() + ifc_file = tool.Ifc.get() + context = bpy.context + obj, datablock_obj = self.create_objects(context) + + # Assign IfcClass + bpy.ops.bim.assign_class( + ifc_class="IfcBuildingElementProxy", predefined_type="ELEMENT", userdefined_type="", props_to_pset=True + ) + element = tool.Ifc.get_entity(obj) + assert element + + # Get Psets + psets = ifcopenshell.util.element.get_psets(element, psets_only=True) + assert "BBIM_ImportedBlenderProps" in psets + pset = psets["BBIM_ImportedBlenderProps"] + assert "01_float" in pset and type(pset["01_float"]) is float + assert "02_float_array.1" in pset and type(pset["02_float_array.1"]) is float + assert "03_integer" in pset and type(pset["03_integer"]) is int + assert "04_integer_array.1" in pset and type(pset["04_integer_array.1"]) is int + assert "05_boolean" in pset and type(pset["05_boolean"]) is bool + assert "06_boolean_array.1" in pset and type(pset["06_boolean_array.1"]) is bool + assert "07_string" in pset and type(pset["07_string"]) is str + assert "08_data_block" not in pset + assert "09_python" not in pset + assert "10_python" not in pset