From b8aa90de007ecf50fa2ac9dd7e22abe5cf9ef7d8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 18 Mar 2022 17:24:59 +1100 Subject: [PATCH] #2094. New rudimentary project materials browser to manage materials. --- .../blenderbim/bim/module/document/ui.py | 2 +- .../bim/module/material/__init__.py | 8 +++ .../blenderbim/bim/module/material/data.py | 63 +++++++++++++++++++ .../bim/module/material/operator.py | 25 +++++--- .../blenderbim/bim/module/material/prop.py | 19 ++++++ .../blenderbim/bim/module/material/ui.py | 42 ++++++++++++- src/blenderbim/blenderbim/bim/ui.py | 2 +- src/blenderbim/blenderbim/core/material.py | 9 +++ src/blenderbim/blenderbim/core/tool.py | 3 + src/blenderbim/blenderbim/tool/material.py | 22 +++++++ .../test/bim/feature/material.feature | 13 ++++ src/blenderbim/test/core/test_drawing.py | 3 +- src/blenderbim/test/core/test_material.py | 13 ++++ src/blenderbim/test/tool/test_material.py | 61 ++++++++++++++++++ 14 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/material/data.py diff --git a/src/blenderbim/blenderbim/bim/module/document/ui.py b/src/blenderbim/blenderbim/bim/module/document/ui.py index ba1b4704b4..b5ee5efe93 100644 --- a/src/blenderbim/blenderbim/bim/module/document/ui.py +++ b/src/blenderbim/blenderbim/bim/module/document/ui.py @@ -29,7 +29,7 @@ class BIM_PT_documents(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_collaboration" + bl_parent_id = "BIM_PT_project_setup" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/material/__init__.py b/src/blenderbim/blenderbim/bim/module/material/__init__.py index dc45495d16..0c42d9c04d 100644 --- a/src/blenderbim/blenderbim/bim/module/material/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/material/__init__.py @@ -31,10 +31,12 @@ classes = ( operator.CopyMaterial, operator.DisableEditingAssignedMaterial, operator.DisableEditingMaterialSetItem, + operator.DisableEditingMaterials, operator.EditAssignedMaterial, operator.EditMaterialSetItem, operator.EnableEditingAssignedMaterial, operator.EnableEditingMaterialSetItem, + operator.LoadMaterials, operator.RemoveConstituent, operator.RemoveLayer, operator.RemoveListItem, @@ -43,15 +45,21 @@ classes = ( operator.ReorderMaterialSetItem, operator.UnassignMaterial, operator.UnlinkMaterial, + prop.Material, + prop.BIMMaterialProperties, prop.BIMObjectMaterialProperties, + ui.BIM_PT_materials, ui.BIM_PT_material, ui.BIM_PT_object_material, + ui.BIM_UL_materials, ) def register(): + bpy.types.Scene.BIMMaterialProperties = bpy.props.PointerProperty(type=prop.BIMMaterialProperties) bpy.types.Object.BIMObjectMaterialProperties = bpy.props.PointerProperty(type=prop.BIMObjectMaterialProperties) def unregister(): + del bpy.types.Scene.BIMMaterialProperties del bpy.types.Object.BIMObjectMaterialProperties diff --git a/src/blenderbim/blenderbim/bim/module/material/data.py b/src/blenderbim/blenderbim/bim/module/material/data.py new file mode 100644 index 0000000000..9d758e685a --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/material/data.py @@ -0,0 +1,63 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import os +import bpy +import ifcopenshell +import ifcopenshell.util.schema +import blenderbim.tool as tool + + +def refresh(): + MaterialsData.is_loaded = False + + +class MaterialsData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "total_materials": cls.total_materials(), + "material_types": cls.material_types(), + } + cls.is_loaded = True + + @classmethod + def total_materials(cls): + return ( + len(tool.Ifc.get().by_type("IfcMaterial")) + + len(tool.Ifc.get().by_type("IfcMaterialConstituentSet")) + + len(tool.Ifc.get().by_type("IfcMaterialLayerSet")) + + len(tool.Ifc.get().by_type("IfcMaterialProfileSet")) + + len(tool.Ifc.get().by_type("IfcMaterialList")) + ) + + @classmethod + def material_types(cls): + material_types = [ + "IfcMaterial", + "IfcMaterialConstituentSet", + "IfcMaterialLayerSet", + "IfcMaterialProfileSet", + "IfcMaterialList", + ] + if tool.Ifc.get_schema() == "IFC2X3": + material_types = ["IfcMaterial", "IfcMaterialLayerSet", "IfcMaterialList"] + return [(m, m, "") for m in material_types] diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index 8fc169ff0e..4fa6685123 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -31,11 +31,22 @@ from ifcopenshell.api.material.data import Data from ifcopenshell.api.profile.data import Data as ProfileData -class Operator: - def execute(self, context): - IfcStore.execute_ifc_operator(self, context) - blenderbim.bim.handler.refresh_ui_data() - return {"FINISHED"} +class LoadMaterials(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.load_materials" + bl_label = "Load Materials" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.load_materials(tool.Material, context.scene.BIMMaterialProperties.material_type) + + +class DisableEditingMaterials(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.disable_editing_materials" + bl_label = "Disable Editing Materials" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.disable_editing_materials(tool.Material) class AssignParameterizedProfile(bpy.types.Operator): @@ -68,7 +79,7 @@ class AssignParameterizedProfile(bpy.types.Operator): return {"FINISHED"} -class AddDefaultMaterial(bpy.types.Operator, Operator): +class AddDefaultMaterial(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_default_material" bl_label = "Add Default Material" bl_options = {"REGISTER", "UNDO"} @@ -131,7 +142,7 @@ class RemoveMaterial(bpy.types.Operator): return {"FINISHED"} -class UnlinkMaterial(bpy.types.Operator, Operator): +class UnlinkMaterial(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unlink_material" bl_label = "Unlink Material" bl_options = {"REGISTER", "UNDO"} diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index 660303d471..9789dd18e7 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -18,6 +18,7 @@ import bpy from ifcopenshell.api.material.data import Data +from blenderbim.bim.module.material.data import MaterialsData from blenderbim.bim.ifc import IfcStore from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup @@ -104,6 +105,24 @@ def getMaterialTypes(self, context): return materialtypes_enum +def get_material_types(self, context): + if not MaterialsData.is_loaded: + MaterialsData.load() + return MaterialsData.data["material_types"] + + +class Material(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + +class BIMMaterialProperties(PropertyGroup): + is_editing: BoolProperty(name="Is Editing", default=False) + material_type: EnumProperty(items=get_material_types, name="Material Type") + materials: CollectionProperty(name="Materials", type=Material) + active_material_index: IntProperty(name="Active Material Index") + + class BIMObjectMaterialProperties(PropertyGroup): material_type: EnumProperty(items=getMaterialTypes, name="Material Type") material: EnumProperty(items=getMaterials, name="Material") diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index f7f51e2f03..24f345a125 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -17,11 +17,44 @@ # along with BlenderBIM Add-on. If not, see . import blenderbim.bim.helper -from bpy.types import Panel +from bpy.types import Panel, UIList from ifcopenshell.api.material.data import Data from ifcopenshell.api.profile.data import Data as ProfileData from blenderbim.bim.ifc import IfcStore from blenderbim.bim.helper import draw_attributes +from blenderbim.bim.module.material.data import MaterialsData + + +class BIM_PT_materials(Panel): + bl_label = "IFC Materials" + bl_idname = "BIM_PT_materials" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_geometry" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + if not MaterialsData.is_loaded: + MaterialsData.load() + + self.props = context.scene.BIMMaterialProperties + + row = self.layout.row(align=True) + row.label(text="{} Materials Found".format(MaterialsData.data["total_materials"]), icon="MATERIAL") + if self.props.is_editing: + row.operator("bim.disable_editing_materials", text="", icon="CANCEL") + else: + row = self.layout.row(align=True) + row.prop(self.props, "material_type", text="") + row.operator("bim.load_materials", text="", icon="IMPORT") + return + + self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index") class BIM_PT_material(Panel): @@ -333,3 +366,10 @@ class BIM_PT_object_material(Panel): if total_thickness: row = self.layout.row(align=True) row.label(text=f"Total Thickness: {total_thickness:.3f}") + + +class BIM_UL_materials(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 83b2ba536d..000ceac13c 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -233,7 +233,7 @@ class BIM_PT_geometry(Panel): class BIM_PT_4D5D(Panel): - bl_label = "IFC 4D/5D" + bl_label = "IFC Costing and Scheduling" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" diff --git a/src/blenderbim/blenderbim/core/material.py b/src/blenderbim/blenderbim/core/material.py index 88c6405e2b..ce2f6466dc 100644 --- a/src/blenderbim/blenderbim/core/material.py +++ b/src/blenderbim/blenderbim/core/material.py @@ -25,3 +25,12 @@ def add_default_material(ifc, material): obj = material.add_default_material_object() ifc.link(ifc.run("material.add_material", name="Default"), obj) return obj + + +def load_materials(material, material_type): + material.import_material_definitions(material_type) + material.enable_editing_materials() + + +def disable_editing_materials(material): + material.disable_editing_materials() diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 5d4471091e..f4a9450609 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -276,6 +276,9 @@ class Library: @interface class Material: def add_default_material_object(cls): pass + def disable_editing_materials(cls): pass + def enable_editing_materials(cls): pass + def import_material_definitions(cls, material_type): pass @interface diff --git a/src/blenderbim/blenderbim/tool/material.py b/src/blenderbim/blenderbim/tool/material.py index 1acb411b62..0c965347fe 100644 --- a/src/blenderbim/blenderbim/tool/material.py +++ b/src/blenderbim/blenderbim/tool/material.py @@ -27,3 +27,25 @@ class Material(blenderbim.core.tool.Material): @classmethod def add_default_material_object(cls): return bpy.data.materials.new("Default") + + @classmethod + def disable_editing_materials(cls): + bpy.context.scene.BIMMaterialProperties.is_editing = False + + @classmethod + def enable_editing_materials(cls): + bpy.context.scene.BIMMaterialProperties.is_editing = True + + @classmethod + def import_material_definitions(cls, material_type): + props = bpy.context.scene.BIMMaterialProperties + props.materials.clear() + for material in tool.Ifc.get().by_type(material_type): + new = props.materials.add() + new.ifc_definition_id = material.id() + if material.is_a("IfcMaterialLayerSet"): + new.name = material.LayerSetName or "Unnamed" + elif material.is_a("IfcMaterialList"): + new.name = "Unnamed" + else: + new.name = material.Name or "Unnamed" diff --git a/src/blenderbim/test/bim/feature/material.feature b/src/blenderbim/test/bim/feature/material.feature index 6516cb5c9a..4dafefaec2 100644 --- a/src/blenderbim/test/bim/feature/material.feature +++ b/src/blenderbim/test/bim/feature/material.feature @@ -1,6 +1,19 @@ @material Feature: Material +Scenario: Load materials + Given an empty IFC project + And I press "bim.add_default_material" + When I press "bim.load_materials" + Then nothing happens + +Scenario: Disable editing materials + Given an empty IFC project + And I press "bim.add_default_material" + And I press "bim.load_materials" + When I press "bim.disable_editing_materials" + Then nothing happens + Scenario: Add default material Given an empty IFC project When I press "bim.add_default_material" diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index 5fb7b822fa..a18d3fa140 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -141,7 +141,8 @@ class TestDisableEditingDrawings: class TestAddDrawing: def test_run(self, ifc, collector, drawing): - drawing.ensure_unique_drawing_name("UNTITLED").should_be_called().will_return("name") + drawing.generate_drawing_name("target_view", "location_hint").should_be_called().will_return("drawing_name") + drawing.ensure_unique_drawing_name("drawing_name").should_be_called().will_return("name") drawing.generate_drawing_matrix("target_view", "location_hint").should_be_called().will_return("matrix") drawing.create_camera("name", "matrix").should_be_called().will_return("obj") drawing.get_body_context().should_be_called().will_return("context") diff --git a/src/blenderbim/test/core/test_material.py b/src/blenderbim/test/core/test_material.py index a35a2fbf6e..86404c1aa6 100644 --- a/src/blenderbim/test/core/test_material.py +++ b/src/blenderbim/test/core/test_material.py @@ -32,3 +32,16 @@ class TestAddDefaultMaterial: ifc.run("material.add_material", name="Default").should_be_called().will_return("material") ifc.link("material", "obj").should_be_called() assert subject.add_default_material(ifc, material) == "obj" + + +class TestLoadMaterials: + def test_run(self, material): + material.import_material_definitions("material_type").should_be_called() + material.enable_editing_materials().should_be_called() + subject.load_materials(material, "material_type") + + +class TestDisableEditingMaterials: + def test_run(self, material): + material.disable_editing_materials().should_be_called() + subject.disable_editing_materials(material) diff --git a/src/blenderbim/test/tool/test_material.py b/src/blenderbim/test/tool/test_material.py index d8c50c09bf..bd739a864e 100644 --- a/src/blenderbim/test/tool/test_material.py +++ b/src/blenderbim/test/tool/test_material.py @@ -34,3 +34,64 @@ class TestAddDefaultMaterialObject(NewFile): material = subject.add_default_material_object() assert isinstance(material, bpy.types.Material) assert material.name == "Default" + + +class TestDisableEditingMaterials(NewFile): + def test_run(self): + bpy.context.scene.BIMMaterialProperties.is_editing = True + subject.disable_editing_materials() + assert bpy.context.scene.BIMMaterialProperties.is_editing is False + + +class TestEnableEditingMaterials(NewFile): + def test_run(self): + bpy.context.scene.BIMMaterialProperties.is_editing = False + subject.enable_editing_materials() + assert bpy.context.scene.BIMMaterialProperties.is_editing is True + + +class TestImportMaterialDefinitions(NewFile): + def test_import_materials(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + material = ifc.createIfcMaterial(Name="Name") + subject.import_material_definitions("IfcMaterial") + props = bpy.context.scene.BIMMaterialProperties + assert props.materials[0].ifc_definition_id == material.id() + assert props.materials[0].name == "Name" + + def test_import_material_layer_sets(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + material = ifc.createIfcMaterialLayerSet(LayerSetName="Name") + subject.import_material_definitions("IfcMaterialLayerSet") + props = bpy.context.scene.BIMMaterialProperties + assert props.materials[0].ifc_definition_id == material.id() + assert props.materials[0].name == "Name" + + def test_import_material_profile_sets(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + material = ifc.createIfcMaterialProfileSet(Name="Name") + subject.import_material_definitions("IfcMaterialProfileSet") + props = bpy.context.scene.BIMMaterialProperties + assert props.materials[0].ifc_definition_id == material.id() + assert props.materials[0].name == "Name" + + def test_import_material_constituent_sets(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + material = ifc.createIfcMaterialConstituentSet(Name="Name") + subject.import_material_definitions("IfcMaterialConstituentSet") + props = bpy.context.scene.BIMMaterialProperties + assert props.materials[0].ifc_definition_id == material.id() + assert props.materials[0].name == "Name" + + def test_import_material_lists(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + material = ifc.createIfcMaterialList() + subject.import_material_definitions("IfcMaterialList") + props = bpy.context.scene.BIMMaterialProperties + assert props.materials[0].ifc_definition_id == material.id() + assert props.materials[0].name == "Unnamed"