From e9a3630595a512d96aab5d77a663ea70e017f0d0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Mar 2023 19:04:32 +1100 Subject: [PATCH] Fix #2898. You can now copy materials from the materials tab. --- .../bim/module/material/operator.py | 44 ++++------- .../blenderbim/bim/module/material/ui.py | 3 +- .../api/material/copy_material.py | 73 +++++++------------ .../test/api/material/test_copy_material.py | 61 ++++++++++++++++ 4 files changed, 104 insertions(+), 77 deletions(-) create mode 100644 src/ifcopenshell-python/test/api/material/test_copy_material.py diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index 839c8986a8..b1d46a8885 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -664,35 +664,23 @@ class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - self.file = IfcStore.get_file() - material = ifcopenshell.util.element.get_material( - self.file.by_id(context.active_object.BIMObjectProperties.ifc_definition_id) - ) - for obj in tool.Blender.get_selected_objects(): - if obj == context.active_object: - continue - if not obj.BIMObjectProperties.ifc_definition_id: - continue - ifcopenshell.api.run( - "material.copy_material", - self.file, - **{ - "material": material, - "element": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), - }, - ) - self.set_default_material(obj, material) + blender_material = context.active_object.active_material + material = tool.Ifc.get_entity(blender_material) + copied_material = ifcopenshell.api.run("material.copy_material", tool.Ifc.get(), material=material) + copied_blender_material = bpy.data.materials.new(blender_material.name) + copied_style = self.get_style(copied_material) + tool.Ifc.link(copied_material, copied_blender_material) + if copied_style: + tool.Ifc.link(copied_style, copied_blender_material) + context.active_object.active_material = copied_blender_material - def set_default_material(self, obj, material): - object_material_ids = [ - om.BIMObjectProperties.ifc_definition_id - for om in obj.data.materials - if om is not None and om.BIMObjectProperties.ifc_definition_id - ] - - if material.id() in object_material_ids: - return - obj.data.materials.append(IfcStore.get_element(material.id())) + def get_style(self, material): + for material_representation in material.HasRepresentation: + for representation in material_representation.Representations: + for item in representation.Items: + for style in item.Styles: + if style.is_a("IfcSurfaceStyle"): + return style class ExpandMaterialCategory(bpy.types.Operator): diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 6112a39c80..2317c3b1d4 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -92,6 +92,7 @@ class BIM_PT_material(Panel): material_id = context.active_object.active_material.BIMObjectProperties.ifc_definition_id if bool(material_id): row.operator("bim.remove_material", icon="X", text="Remove IFC Material").material = material_id + row.operator("bim.copy_material", icon="DUPLICATE", text="") row.operator("bim.unlink_material", icon="UNLINKED", text="") else: op = row.operator("bim.add_material", icon="ADD", text="Create IFC Material") @@ -158,8 +159,6 @@ class BIM_PT_object_material(Panel): op.material_set_usage = ObjectMaterialData.data["material_id"] row.operator("bim.disable_editing_assigned_material", icon="CANCEL", text="") else: - if ObjectMaterialData.data["material_class"] == "IfcMaterial": - row.operator("bim.copy_material", icon="COPYDOWN", text="") row.operator("bim.enable_editing_assigned_material", icon="GREASEPENCIL", text="") row.operator("bim.unassign_material", icon="X", text="") diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/copy_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/copy_material.py index 6778a432ff..cd49c73e8f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/copy_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/copy_material.py @@ -21,65 +21,44 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, material=None, element=None): - """Copies a material to an element + def __init__(self, file, material=None): + """Copies a material - This convenience function exists to allow you to easily unassign any - existing material an element has, and assign a new material to it. - Essentially, it is a form a copy / pasting a material from one element - to another. + All material psets and styles are copied. The copied material is not + associated to any elements. - It's a bit out of place, and will likely be redesigned or removed in the - future. It's not recommended to use this function, and - ifcopenshell.api.material.assign_material should be used instead. - - :param material: The IfcMaterial to assign to the element. + :param material: The IfcMaterial to copy :type material: ifcopenshell.entity_instance.entity_instance - :param element: The IfcElement or IfcElementType to remove all previous - materials from and assign the new material to. - :type element: ifcopenshell.entity_instance.entity_instance - :return: None - :rtype: None + :return: The new copy of the material + :rtype: ifcopenshell.entity_instance.entity_instance Example: .. code:: python concrete = ifcopenshell.api.run("material.add_material", model, name="CON01", category="concrete") - wood = ifcopenshell.api.run("material.add_material", model, name="TIM01", category="wood") - # Let's imagine a concrete bench made out of concrete. - bench = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture") - ifcopenshell.api.run("material.assign_material", model, - product=bench, type="IfcMaterial", material=concrete) - - # And some street furniture made from wood. - street_furniture = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture") - ifcopenshell.api.run("material.assign_material", model, - product=street_furniture, type="IfcMaterial", material=wood) - - # Let's copy the concrete over to the street furniture. - ifcopenshell.api.run("material.copy_material", model, material=wood, element=street_furniture) + # Let's duplicate the concrete material + concrete_copy = ifcopenshell.api.run("material.copy_material", model, material=concrete) """ self.file = file - self.settings = {"material": material, "element": element} + self.settings = {"material": material} def execute(self): - ifcopenshell.api.run("material.unassign_material", self.file, product=self.settings["element"]) if self.settings["material"].is_a("IfcMaterial"): - ifcopenshell.api.run( - "material.assign_material", - self.file, - product=self.settings["element"], - type="IfcMaterial", - material=self.settings["material"], - ) - # No other material type can be copied right now. - # 1. Material lists and constituents may have shape aspects and I - # haven't implemented it yet. - # 2. Material layer and profile sets implicitly define parametric - # geometry and we have no way of guaranteeing that this constraint is - # satisfied. - # 3. Material set usages follow an unofficial constraint that all - # instances must have a usage of their type's material set. We cannot - # guarantee that constraint. + new = ifcopenshell.util.element.copy(self.file, self.settings["material"]) + for inverse in self.file.get_inverse(self.settings["material"]): + if inverse.is_a("IfcMaterialProperties"): + # Properties must not be shared between objects for convenience of authoring + inverse = ifcopenshell.util.element.copy(self.file, inverse) + properties = [] + for pset in inverse.Properties: + properties.append(ifcopenshell.util.element.copy_deep(self.file, pset)) + inverse.Properties = properties + inverse.Material = new + elif inverse.is_a("IfcMaterialDefinitionRepresentation"): + inverse = ifcopenshell.util.element.copy_deep( + self.file, inverse, exclude=["IfcRepresentationContext", "IfcMaterial"] + ) + inverse.RepresentedMaterial = new + return new diff --git a/src/ifcopenshell-python/test/api/material/test_copy_material.py b/src/ifcopenshell-python/test/api/material/test_copy_material.py new file mode 100644 index 0000000000..ea172ca163 --- /dev/null +++ b/src/ifcopenshell-python/test/api/material/test_copy_material.py @@ -0,0 +1,61 @@ +# 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 . + +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element + + +class TestCopyMaterial(test.bootstrap.IFC4): + def test_copy_a_single_material(self): + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + new = ifcopenshell.api.run("material.copy_material", self.file, material=material) + assert new.Name == "CON01" + assert len(self.file.by_type("IfcMaterial")) == 2 + + def test_assignments_are_not_copied(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + new = ifcopenshell.api.run("material.copy_material", self.file, material=material) + assert material.AssociatedTo + assert not new.AssociatedTo + + def test_copy_a_material_with_properties(self): + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=material, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"foo": "bar"}) + new = ifcopenshell.api.run("material.copy_material", self.file, material=material) + assert new.Name == "CON01" + assert len(self.file.by_type("IfcMaterial")) == 2 + assert new.HasProperties[0] != material.HasProperties[0] + assert new.HasProperties[0].Properties[0] != material.HasProperties[0].Properties[0] + assert ifcopenshell.util.element.get_pset(new, "Foo_Bar", "foo") == "bar" + + def test_copy_a_material_with_a_style_representation(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + context = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + style = ifcopenshell.api.run("style.add_style", self.file) + ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=context) + new = ifcopenshell.api.run("material.copy_material", self.file, material=material) + assert new.Name == "CON01" + assert len(self.file.by_type("IfcMaterialDefinitionRepresentation")) == 2 + assert new.HasRepresentation[0] != material.HasRepresentation[0] + assert new.HasRepresentation[0].Representations[0] != material.HasRepresentation[0].Representations[0] + assert new.HasRepresentation[0].Representations[0].ContextOfItems == context