From bba862e3aefeb8c3d51ec28751e4db103b6b7f0a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 9 Oct 2024 18:27:36 +1100 Subject: [PATCH] Fix #5544. Assign / add of objects with styles are now supported. --- src/bonsai/bonsai/bim/module/root/operator.py | 18 +-- src/bonsai/bonsai/tool/geometry.py | 32 +++++ src/bonsai/bonsai/tool/style.py | 50 +------ .../ifcopenshell/api/style/__init__.py | 2 + .../api/style/assign_item_style.py | 132 ++++++++++++++++++ 5 files changed, 170 insertions(+), 64 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/assign_item_style.py diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 47d440012a..a9ddbd6a4d 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -210,12 +210,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): ifc_representation_class=self.ifc_representation_class, ) if self.should_add_representation and obj.data and len(obj.data.vertices): - builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - verts = [v.co / unit_scale for v in obj.data.vertices] - faces = [p.vertices[:] for p in obj.data.polygons] - item = builder.mesh(verts, faces) - representation = builder.get_representation(ifc_context, [item]) + representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context) ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation) bonsai.core.geometry.switch_representation( tool.Ifc, @@ -373,16 +368,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): pass elif representation_template == "OBJ" and props.representation_obj: obj.matrix_world = props.representation_obj.matrix_world - builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - items = [] - meshes = tool.Geometry.split_by_loose_parts(props.representation_obj) - for mesh in meshes: - verts = [v.co / unit_scale for v in mesh.vertices] - faces = [p.vertices[:] for p in mesh.polygons] - items.append(builder.mesh(verts, faces)) - bpy.data.meshes.remove(mesh) - representation = builder.get_representation(ifc_context, items) + representation = tool.Geometry.export_mesh_to_tessellation(props.representation_obj, ifc_context) ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation) bonsai.core.geometry.switch_representation( tool.Ifc, diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 83b510aa76..9d16c26484 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -27,6 +27,7 @@ import multiprocessing import ifcopenshell import ifcopenshell.api import ifcopenshell.api.grid +import ifcopenshell.api.style import ifcopenshell.geom import ifcopenshell.guid import ifcopenshell.util.element @@ -1600,3 +1601,34 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def reload_representation_item_ids(cls, representation: ifcopenshell.entity_instance, data: bpy.types.Mesh) -> None: data["ios_item_ids"] = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)] + + @classmethod + def export_mesh_to_tessellation(cls, obj: bpy.types.Object, ifc_context) -> ifcopenshell.entity_instance: + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + items = [] + meshes = cls.split_by_loose_parts(obj) + for mesh in meshes: + verts = [v.co / unit_scale for v in mesh.vertices] + faces = [p.vertices[:] for p in mesh.polygons] + item = builder.mesh(verts, faces) + items.append(item) + material_index = mesh.polygons[0].material_index + if materials := list(mesh.materials): + material = materials[material_index] + if not (style := tool.Style.get_style(material)): + style = ifcopenshell.api.run("style.add_style", tool.Ifc.get(), name=material.name) + if material.use_nodes: + ifc_class = "IfcSurfaceStyleRendering" + attributes = tool.Style.get_surface_rendering_attributes(material) + else: + ifc_class = "IfcSurfaceStyleShading" + attributes = tool.Style.get_surface_shading_attributes(material) + ifcopenshell.api.style.add_surface_style( + tool.Ifc.get(), style=style, ifc_class=ifc_class, attributes=attributes + ) + tool.Ifc.link(style, material) + material.use_fake_user = True + ifcopenshell.api.style.assign_item_style(tool.Ifc.get(), item=item, style=style) + bpy.data.meshes.remove(mesh) + return builder.get_representation(ifc_context, items) diff --git a/src/bonsai/bonsai/tool/style.py b/src/bonsai/bonsai/tool/style.py index a1be3b1c28..7daf3d8739 100644 --- a/src/bonsai/bonsai/tool/style.py +++ b/src/bonsai/bonsai/tool/style.py @@ -594,55 +594,9 @@ class Style(bonsai.core.tool.Style): @classmethod def assign_style_to_representation_item( - cls, representation_item: ifcopenshell.entity_instance, style: Optional[ifcopenshell.entity_instance] = None + cls, item: ifcopenshell.entity_instance, style: Optional[ifcopenshell.entity_instance] = None ) -> None: - """Assign specified style to a representation item and unassign all previously assigned styles.""" - ifc_file = representation_item.file - if not (styled_item := next(iter(representation_item.StyledByItem), None)): - if style is None: - return - if ifc_file.schema == "IFC2X3": - style = ifc_file.create_entity("IfcPresentationStyleAssignment", (style,)) - ifc_file.create_entity("IfcStyledItem", representation_item, (style,)) - return - - styled_item_styles = styled_item.Styles - if style and styled_item_styles == (style,): - return - - if ifc_file.schema == "IFC4X3": - if style is None: - ifc_file.remove(styled_item) - return - styled_item.Styles = (style,) - return - - # < IFC4X3 - # Can't just remove a styled item or assign a style - # since we need to remove/change the possible style assignments. - assignment = None - for style_ in styled_item_styles: - if not style_.is_a("IfcPresentationStyleAssignment"): - continue - # Remove second assignment. - if style is None or assignment: - ifc_file.remove(style_) - else: - assignment = style_ - if assignment.Styles != (style,): - assignment.Styles = (style,) - - if style is None: - ifc_file.remove(styled_item) - return - - if assignment: - if styled_item_styles == (assignment,): - return - styled_item.Styles = (assignment,) - return - - styled_item.Styles = (style,) + return ifcopenshell.api.style.assign_item_style(tool.Ifc.get(), item=item, style=style) @classmethod def get_representation_item_style( diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/style/__init__.py index ca84bad55a..c951cfabb4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/__init__.py @@ -27,6 +27,7 @@ from .. import wrap_usecases from .add_style import add_style from .add_surface_style import add_surface_style from .add_surface_textures import add_surface_textures +from .assign_item_style import assign_item_style from .assign_material_style import assign_material_style from .assign_representation_styles import assign_representation_styles from .edit_presentation_style import edit_presentation_style @@ -43,6 +44,7 @@ __all__ = [ "add_style", "add_surface_style", "add_surface_textures", + "assign_item_style", "assign_material_style", "assign_representation_styles", "edit_presentation_style", diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/assign_item_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/assign_item_style.py new file mode 100644 index 0000000000..5e77a27920 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/style/assign_item_style.py @@ -0,0 +1,132 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 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 ifcopenshell +from typing import Optional, Union + + +def assign_item_style( + file: ifcopenshell.file, + item: ifcopenshell.entity_instance, + style: Optional[ifcopenshell.entity_instance], + should_use_presentation_style_assignment: bool = False, +) -> Union[ifcopenshell.entity_instance, None]: + """Assigns a style directly to a representation item + + A style may either be assigned directly to an object's representation + items, or to a material which is then associated with the object. If both + exist, then the style assigned directly to the object's representation + takes precedence. It is recommended to use materials and assign styles to + materials. However, sometimes you may want to assign colours directly to + the object representation as an override. This API function provides that + capability. + + If you want to assign styles to a material instead (recommended), then + please see ifcopenshell.api.style.assign_material_style. + + :param item: The IfcRepresentationItem of the object + that you want to assign styles to. + :param style: A presentation style, typically IfcSurfaceStyle. None if you + want to remove an existing style from the item. + :return: Created or existing IfcStyledItem, or None if the style was removed. + + Example: + + .. code:: python + + # A model context is needed to store 3D geometry + model3d = ifcopenshell.api.context.add_context(model, context_type="Model") + + # Specifically, we want to store body geometry + body = ifcopenshell.api.context.add_context(model, + context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d) + + # Let's create a new wall. The wall does not have any geometry yet. + wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") + + # Let's use the "3D Body" representation we created earlier to add a + # new wall-like body geometry, 5 meters long, 3 meters high, and + # 200mm thick + representation = ifcopenshell.api.geometry.add_wall_representation(model, + context=body, length=5, height=3, thickness=0.2) + + # Assign our new body geometry back to our wall + ifcopenshell.api.geometry.assign_representation(model, + product=wall, representation=representation) + + # Place our wall at the origin + ifcopenshell.api.geometry.edit_object_placement(model, product=wall) + + # Create a new surface style + style = ifcopenshell.api.style.add_style(model) + + # Create a simple grey shading colour and transparency. + ifcopenshell.api.style.add_surface_style(model, + style=style, ifc_class="IfcSurfaceStyleShading", attributes={ + "SurfaceColour": { "Name": None, "Red": 0.5, "Green": 0.5, "Blue": 0.5 }, + "Transparency": 0., # 0 is opaque, 1 is transparent + }) + + # Now specifically our wall only will be coloured grey. + ifcopenshell.api.style.assign_representation_styles(model, + shape_representation=representation, styles=[style]) + """ + if not (styled_item := next(iter(item.StyledByItem), None)): + if style is None: + return + if file.schema == "IFC2X3": + style = file.create_entity("IfcPresentationStyleAssignment", (style,)) + return file.create_entity("IfcStyledItem", item, (style,)) + + styled_item_styles = styled_item.Styles + if style and styled_item_styles == (style,): + return styled_item + + if file.schema == "IFC4X3": + if style is None: + file.remove(styled_item) + return + styled_item.Styles = (style,) + return styled_item + + # < IFC4X3 + # Can't just remove a styled item or assign a style + # since we need to remove/change the possible style assignments. + assignment = None + for style_ in styled_item_styles: + if not style_.is_a("IfcPresentationStyleAssignment"): + continue + # Remove second assignment. + if style is None or assignment: + file.remove(style_) + else: + assignment = style_ + if assignment.Styles != (style,): + assignment.Styles = (style,) + + if style is None: + file.remove(styled_item) + return + + if assignment: + if styled_item_styles == (assignment,): + return styled_item + styled_item.Styles = (assignment,) + return styled_item + + styled_item.Styles = (style,) + return styled_item