From f10d340254891f3e3a79e3591580138946eae457 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 21 Oct 2023 15:23:43 +1100 Subject: [PATCH] You can now assign and unassign styles to materials --- .../bim/module/material/__init__.py | 3 + .../blenderbim/bim/module/material/data.py | 55 +++++++++++++++ .../bim/module/material/operator.py | 47 +++++++++++++ .../blenderbim/bim/module/material/prop.py | 17 ++++- .../blenderbim/bim/module/material/ui.py | 59 +++++++++++----- src/blenderbim/blenderbim/tool/material.py | 1 + .../api/style/remove_styled_representation.py | 54 ++++++++++++++ .../api/style/unassign_material_style.py | 70 +++++++++++++++++++ 8 files changed, 286 insertions(+), 20 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/remove_styled_representation.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/unassign_material_style.py diff --git a/src/blenderbim/blenderbim/bim/module/material/__init__.py b/src/blenderbim/blenderbim/bim/module/material/__init__.py index 958ff75add..8757f86c55 100644 --- a/src/blenderbim/blenderbim/bim/module/material/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/material/__init__.py @@ -40,10 +40,12 @@ classes = ( operator.EditMaterial, operator.EditMaterialSetItem, operator.EditMaterialSetItemProfile, + operator.EditMaterialStyle, operator.EnableEditingAssignedMaterial, operator.EnableEditingMaterial, operator.EnableEditingMaterialSetItem, operator.EnableEditingMaterialSetItemProfile, + operator.EnableEditingMaterialStyle, operator.ExpandMaterialCategory, operator.LoadMaterials, operator.RemoveConstituent, @@ -55,6 +57,7 @@ classes = ( operator.ReorderMaterialSetItem, operator.SelectByMaterial, operator.UnassignMaterial, + operator.UnassignMaterialStyle, operator.UnlinkMaterial, prop.Material, prop.BIMMaterialProperties, diff --git a/src/blenderbim/blenderbim/bim/module/material/data.py b/src/blenderbim/blenderbim/bim/module/material/data.py index 7d96782ff3..854c01da9b 100644 --- a/src/blenderbim/blenderbim/bim/module/material/data.py +++ b/src/blenderbim/blenderbim/bim/module/material/data.py @@ -38,6 +38,9 @@ class MaterialsData: "total_materials": cls.total_materials(), "material_types": cls.material_types(), "profiles": cls.profiles(), + "styles": cls.styles(), + "contexts": cls.contexts(), + "active_styles": cls.active_styles(), } cls.is_loaded = True @@ -79,6 +82,58 @@ class MaterialsData: if p.ProfileName ] + @classmethod + def contexts(cls): + results = [] + for element in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + results.append((str(element.id()), element.ContextType or "Unnamed", "")) + for element in tool.Ifc.get().by_type("IfcGeometricRepresentationSubContext", include_subtypes=False): + results.append( + ( + str(element.id()), + "{}/{}/{}".format( + element.ContextType or "Unnamed", + element.ContextIdentifier or "Unnamed", + element.TargetView or "Unnamed", + ), + "", + ) + ) + return results + + @classmethod + def styles(cls): + return [(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcSurfaceStyle") if s.Name] + + @classmethod + def active_styles(cls): + props = bpy.context.scene.BIMMaterialProperties + results = [] + if props.materials and props.active_material_index < len(props.materials): + material = props.materials[props.active_material_index].ifc_definition_id + if not material: + return results + material = tool.Ifc.get().by_id(material) + for definition in material.HasRepresentation: + for representation in definition.Representations: + if not representation.is_a("IfcStyledRepresentation"): + continue + context = representation.ContextOfItems + for item in representation.Items: + if not item.is_a("IfcStyledItem"): + continue + for style in item.Styles: + if style.is_a("IfcSurfaceStyle"): + results.append({ + "context_type": context.ContextType, + "context_identifier": getattr(context, "ContextIdentifier", ""), + "target_view": getattr(context, "TargetView", ""), + "name": style.Name or "Unnamed", + "id": style.id(), + "context_id": context.id(), + }) + return results + class ObjectMaterialData: data = {} diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index 846fc3db40..b41a862252 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -715,3 +715,50 @@ class ContractMaterialCategory(bpy.types.Operator): category.is_expanded = False core.load_materials(tool.Material, props.material_type) return {"FINISHED"} + + +class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_material_style" + bl_label = "Enable Editing Material Style" + bl_options = {"REGISTER", "UNDO"} + material: bpy.props.IntProperty() + + def _execute(self, context): + props = bpy.context.scene.BIMMaterialProperties + props.active_material_id = self.material + props.editing_material_type = "STYLE" + + +class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_material_style" + bl_label = "Edit Material Style" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = bpy.context.scene.BIMMaterialProperties + material = tool.Ifc.get().by_id(props.active_material_id) + style = tool.Ifc.get().by_id(int(props.styles)) + context = tool.Ifc.get().by_id(int(props.contexts)) + ifcopenshell.api.run( + "style.assign_material_style", tool.Ifc.get(), material=material, style=style, context=context + ) + tool.Material.disable_editing_material() + core.load_materials(tool.Material, props.material_type) + + +class UnassignMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.unassign_material_style" + bl_label = "Unassign Material Style" + bl_options = {"REGISTER", "UNDO"} + style: bpy.props.IntProperty() + context: bpy.props.IntProperty() + + def _execute(self, context): + props = bpy.context.scene.BIMMaterialProperties + material = tool.Ifc.get().by_id(props.materials[props.active_material_index].ifc_definition_id) + style = tool.Ifc.get().by_id(self.style) + context = tool.Ifc.get().by_id(self.context) + ifcopenshell.api.run( + "style.unassign_material_style", tool.Ifc.get(), material=material, style=style, context=context + ) + core.load_materials(tool.Material, props.material_type) diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index f09075cdf0..f414a8138e 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -103,11 +103,24 @@ def get_profiles(self, context): return MaterialsData.data["profiles"] +def get_styles(self, context): + if not MaterialsData.is_loaded: + MaterialsData.load() + return MaterialsData.data["styles"] + + +def get_contexts(self, context): + if not MaterialsData.is_loaded: + MaterialsData.load() + return MaterialsData.data["contexts"] + + class Material(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") is_category: BoolProperty(name="Is Category", default=False) is_expanded: BoolProperty(name="Is Expanded", default=True) + has_style: BoolProperty(name="Has Style", default=True) total_elements: IntProperty(name="Total Elements") @@ -119,7 +132,9 @@ class BIMMaterialProperties(PropertyGroup): profiles: EnumProperty(items=get_profiles, name="Profiles") active_material_id: IntProperty(name="Active Material ID") material_attributes: CollectionProperty(name="Material Attributes", type=Attribute) - editing_material_type = StringProperty(name="Editing Material Type") + editing_material_type: StringProperty(name="Editing Material Type") + styles: EnumProperty(items=get_styles, name="Styles") + contexts: EnumProperty(items=get_contexts, name="Contexts") class BIMObjectMaterialProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 8eddab9856..3a755a9413 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -57,26 +57,19 @@ class BIM_PT_materials(Panel): row.alignment = "RIGHT" if self.props.material_type == "IfcMaterial": - if not self.props.active_material_id: - row.operator("bim.add_material", text="", icon="ADD") + row.operator("bim.add_material", text="", icon="ADD") if self.props.materials and self.props.active_material_index < len(self.props.materials): material = self.props.materials[self.props.active_material_index] if material.ifc_definition_id: - if self.props.active_material_id: - row.operator( - "bim.edit_material", text="", icon="CHECKMARK" - ).material = material.ifc_definition_id - row.operator( - "bim.disable_editing_material", text="", icon="CANCEL" - ).material = material.ifc_definition_id - self.draw_editable_material_attributes_ui() - else: - op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF") - op.material = material.ifc_definition_id - row.operator( - "bim.enable_editing_material", text="", icon="GREASEPENCIL" - ).material = material.ifc_definition_id - row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id + op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF") + op.material = material.ifc_definition_id + op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL") + op.material = material.ifc_definition_id + op = row.operator("bim.enable_editing_material_style", text="", icon="SHADING_RENDERED") + op.material = material.ifc_definition_id + row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id + + self.draw_editing_ui() else: row.operator("bim.add_material_set", text="", icon="ADD").set_type = self.props.material_type if self.props.materials and self.props.active_material_index < len(self.props.materials): @@ -88,8 +81,33 @@ class BIM_PT_materials(Panel): self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index") - def draw_editable_material_attributes_ui(self): - blenderbim.bim.helper.draw_attributes(self.props.material_attributes, self.layout) + for style in MaterialsData.data["active_styles"]: + row = self.layout.row(align=True) + row.label(text="", icon="SHADING_RENDERED") + row.label(text=style["context_type"]) + row.label(text=style["context_identifier"]) + row.label(text=style["target_view"]) + row.label(text=style["name"]) + op = row.operator("bim.unassign_material_style", text="", icon="X") + op.style = style["id"] + op.context = style["context_id"] + + def draw_editing_ui(self): + if not self.props.active_material_id: + return + ifc_definition_id = self.props.active_material_id + if self.props.editing_material_type == "ATTRIBUTES": + blenderbim.bim.helper.draw_attributes(self.props.material_attributes, self.layout) + row = self.layout.row(align=True) + row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id + row.operator("bim.disable_editing_material", text="", icon="CANCEL") + elif self.props.editing_material_type == "STYLE": + row = self.layout.row(align=True) + row.prop(self.props, "contexts", text="") + row.prop(self.props, "styles", text="") + row = self.layout.row(align=True) + row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK") + row.operator("bim.disable_editing_material", text="", icon="CANCEL") class BIM_PT_material(Panel): @@ -343,3 +361,6 @@ class BIM_UL_materials(UIList): row2 = row.row() row2.alignment = "RIGHT" row2.label(text=str(item.total_elements)) + + if item.has_style: + row2.label(text="", icon="SHADING_RENDERED") diff --git a/src/blenderbim/blenderbim/tool/material.py b/src/blenderbim/blenderbim/tool/material.py index 1f0150bbfe..ea790996ee 100644 --- a/src/blenderbim/blenderbim/tool/material.py +++ b/src/blenderbim/blenderbim/tool/material.py @@ -80,6 +80,7 @@ class Material(blenderbim.core.tool.Material): new.total_elements = len( ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material) ) + new.has_style = bool(material.HasRepresentation) return for material in materials: new = props.materials.add() diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/remove_styled_representation.py b/src/ifcopenshell-python/ifcopenshell/api/style/remove_styled_representation.py new file mode 100644 index 0000000000..4381f91a4b --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/style/remove_styled_representation.py @@ -0,0 +1,54 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + + +class Usecase: + def __init__(self, file, representation=None): + """Removes a styled representation + + Styled representations are typically associated with materials. This + removes the representation but not the underlying styles. + + :param representation: The IfcStyledRepresentation to remove. + :type representation: ifcopenshell.entity_instance.entity_instance + :return: None + :rtype: None + + Example: + + .. code:: python + + # Remove a styled representation + ifcopenshell.api.run("style.remove_styled_representation", model, representation=representation) + """ + self.file = file + self.settings = {"representation": representation} + + def execute(self): + for inverse in self.file.get_inverse(self.settings["representation"]): + if inverse.is_a("IfcMaterialDefinitionRepresentation") and len(inverse.Representations) == 1: + self.file.remove(inverse) + + for item in self.settings["representation"].Items: + if item.is_a("IfcStyledItem") and self.file.get_total_inverses(item) == 1: + for style in item.Styles: + if style.is_a("IfcPresentationStyleAssignment"): + self.file.remove(style) + self.file.remove(item) + + self.file.remove(self.settings["representation"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_material_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_material_style.py new file mode 100644 index 0000000000..f88030ad9b --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_material_style.py @@ -0,0 +1,70 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + + +class Usecase: + def __init__(self, file, material=None, style=None, context=None): + """Unassigns a style to a material + + This does the inverse of assign_material_style. + + :param material: The IfcMaterial which you want to unassign the style from. + :type material: ifcopenshell.entity_instance.entity_instance + :param style: The IfcPresentationStyle (typically IfcSurfaceStyle) that + you want to unassign from material. This will then be applied to all + objects that have that material. + :type style: ifcopenshell.entity_instance.entity_instance + :param context: The IfcGeometricRepresentationSubContext at which this + style should be unassigned. Typically this is the Model BODY context. + :type context: ifcopenshell.entity_instance.entity_instance + :return: None + :rtype: None + + Example: + + .. code:: python + + ifcopenshell.api.run("style.unassign_material_style", model, material=concrete, style=style, context=body) + """ + self.file = file + self.settings = { + "material": material, + "style": style, + "context": context, + } + + def execute(self): + to_delete = set() + for definition in self.settings["material"].HasRepresentation: + for representation in definition.Representations: + if not representation.is_a("IfcStyledRepresentation"): + continue + if representation.ContextOfItems != self.settings["context"]: + continue + for item in representation.Items: + if not item.is_a("IfcStyledItem"): + continue + styles = [s for s in item.Styles if s != self.settings["style"]] + if not styles: + self.file.remove(item) + elif len(styles) != len(item.Styles): + item.Styles = styles + if not representation.Items: + self.file.remove(representation) + if not definition.Representations: + self.file.remove(definition)