|
| 1 | +# IfcOpenShell - IFC toolkit and geometry engine |
| 2 | +# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com> |
| 3 | +# |
| 4 | +# This file is part of IfcOpenShell. |
| 5 | +# |
| 6 | +# IfcOpenShell is free software: you can redistribute it and/or modify |
| 7 | +# it under the terms of the GNU Lesser General Public License as published by |
| 8 | +# the Free Software Foundation, either version 3 of the License, or |
| 9 | +# (at your option) any later version. |
| 10 | +# |
| 11 | +# IfcOpenShell is distributed in the hope that it will be useful, |
| 12 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +# GNU Lesser General Public License for more details. |
| 15 | +# |
| 16 | +# You should have received a copy of the GNU Lesser General Public License |
| 17 | +# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. |
| 18 | + |
| 19 | +# This file was generated with the assistance of an AI coding tool. |
| 20 | + |
| 21 | +import ifcopenshell |
| 22 | +import ifcopenshell.api.material |
| 23 | + |
| 24 | + |
| 25 | +def merge_materials( |
| 26 | + file: ifcopenshell.file, |
| 27 | + materials: list[ifcopenshell.entity_instance], |
| 28 | + merge_into: ifcopenshell.entity_instance, |
| 29 | +) -> None: |
| 30 | + """Merges one or more redundant materials into a single designated material |
| 31 | +
|
| 32 | + This is typically used to clean up models (often from import processes) |
| 33 | + that end up with many duplicate or near-duplicate IfcMaterial |
| 34 | + definitions. Every product, type, material layer, material profile, |
| 35 | + material constituent, and material list item that currently references |
| 36 | + one of `materials` will be repointed to `merge_into` instead. Once a |
| 37 | + material has no more references, it is removed, along with any |
| 38 | + properties or styles that belonged only to that redundant material (the |
| 39 | + definition of `merge_into` "wins"). |
| 40 | +
|
| 41 | + If `merge_into` is included in `materials`, it is simply skipped, so it |
| 42 | + is safe to pass the designated material as part of a "select all, merge" |
| 43 | + workflow. |
| 44 | +
|
| 45 | + :param materials: The list of redundant IfcMaterial entities to merge and |
| 46 | + remove. |
| 47 | + :param merge_into: The IfcMaterial entity that should be used instead of |
| 48 | + all of `materials`. |
| 49 | + :return: None |
| 50 | +
|
| 51 | + Example: |
| 52 | +
|
| 53 | + .. code:: python |
| 54 | +
|
| 55 | + # Two suspiciously similar materials, perhaps from a bad import. |
| 56 | + m1 = ifcopenshell.api.material.add_material(model, name="Concrete") |
| 57 | + m2 = ifcopenshell.api.material.add_material(model, name="Concrete_2") |
| 58 | +
|
| 59 | + wall1 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") |
| 60 | + wall2 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") |
| 61 | + ifcopenshell.api.material.assign_material(model, products=[wall1], material=m1) |
| 62 | + ifcopenshell.api.material.assign_material(model, products=[wall2], material=m2) |
| 63 | +
|
| 64 | + # Both walls will now use m1, and m2 will no longer exist. |
| 65 | + ifcopenshell.api.material.merge_materials(model, materials=[m1, m2], merge_into=m1) |
| 66 | + """ |
| 67 | + materials_to_remove = [m for m in materials if m != merge_into] |
| 68 | + if not materials_to_remove: |
| 69 | + return |
| 70 | + |
| 71 | + for material in materials_to_remove: |
| 72 | + # Repoint any direct "this product is made of this material" relationship. |
| 73 | + rel = next( |
| 74 | + ( |
| 75 | + r |
| 76 | + for r in file.get_inverse(material) |
| 77 | + if r.is_a("IfcRelAssociatesMaterial") and r.RelatingMaterial == material |
| 78 | + ), |
| 79 | + None, |
| 80 | + ) |
| 81 | + if rel is not None and rel.RelatedObjects: |
| 82 | + ifcopenshell.api.material.assign_material( |
| 83 | + file, products=list(rel.RelatedObjects), type="IfcMaterial", material=merge_into |
| 84 | + ) |
| 85 | + |
| 86 | + # Repoint material set membership (layers, profiles, constituents, lists). |
| 87 | + for inverse in file.get_inverse(material): |
| 88 | + if ( |
| 89 | + inverse.is_a("IfcMaterialLayer") |
| 90 | + or inverse.is_a("IfcMaterialProfile") |
| 91 | + or inverse.is_a("IfcMaterialConstituent") |
| 92 | + ): |
| 93 | + inverse.Material = merge_into |
| 94 | + elif inverse.is_a("IfcMaterialList"): |
| 95 | + new_materials = [] |
| 96 | + for item in inverse.Materials or []: |
| 97 | + item = merge_into if item == material else item |
| 98 | + if item not in new_materials: |
| 99 | + new_materials.append(item) |
| 100 | + inverse.Materials = new_materials |
| 101 | + |
| 102 | + # Whatever is left (e.g. properties or a style definition that |
| 103 | + # belonged only to this now-redundant material) is discarded along |
| 104 | + # with the material itself. |
| 105 | + ifcopenshell.api.material.remove_material(file, material=material) |
0 commit comments