diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 9cea2cca2e..e0679a18c2 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -55,10 +55,10 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - return if isinstance(obj, bpy.types.Material): - props = obj.BIMStyleProperties + props = tool.Style.get_material_style_props(obj) if ifc_definition_id := props.ifc_definition_id: if props.is_renaming: - props.is_renmaing = False + props.is_renaming = False return tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name refresh_ui_data() diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index ce84c66f02..281d39df8e 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -265,7 +265,8 @@ class IfcStore: IfcStore.guid_map[global_id] = obj if element.is_a("IfcSurfaceStyle"): - obj.BIMStyleProperties.ifc_definition_id = element.id() + props = tool.Style.get_material_style_props(obj) + props.ifc_definition_id = element.id() else: props = tool.Blender.get_object_bim_props(obj) props.ifc_definition_id = element.id() @@ -406,7 +407,8 @@ class IfcStore: @staticmethod def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None: if isinstance(obj, bpy.types.Material): - obj.BIMStyleProperties.ifc_definition_id = 0 + props = tool.Style.get_material_style_props(obj) + props.ifc_definition_id = 0 else: # bpy.types.Object props = tool.Blender.get_object_bim_props(obj) props.ifc_definition_id = 0 diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 0ed6f3d9e7..ade2a99143 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -86,7 +86,7 @@ class MaterialCreator: def load_existing_materials(self) -> None: for material in bpy.data.materials: - if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id: + if ifc_definition_id := tool.Blender.get_ifc_definition_id(material): self.styles[ifc_definition_id] = material def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None: @@ -964,10 +964,11 @@ class IfcImporter: self.material_creator.styles[style.id()] = blender_material style_elements = tool.Style.get_style_elements(blender_material) + props = tool.Style.get_material_style_props(blender_material) if tool.Style.has_blender_external_style(style_elements): - blender_material.BIMStyleProperties.active_style_type = "External" + props.active_style_type = "External" else: - blender_material.BIMStyleProperties.active_style_type = "Shading" + props.active_style_type = "Shading" def place_objects_in_collections(self) -> None: for ifc_definition_id, obj in self.added_data.items(): diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 2b75406d5c..42e17cf063 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3353,7 +3353,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator): obj.material_slots[0].material = material bpy.ops.bim.add_style() - style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id) + style = tool.Ifc.get_entity(material) + assert style tool.Style.assign_style_to_object(style, obj) # TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 22f7fa5071..9b12dbad55 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -1019,7 +1019,7 @@ class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator): position = Matrix() direction = Vector(extrusion.ExtrudedDirection.DirectionRatios).normalized() - tool.Model.import_axis([Vector((0, 0, 0)), direction * extrusion.Depth], obj=obj, position=position) + tool.Model.import_axis((Vector((0, 0, 0)), direction * extrusion.Depth), obj=obj, position=position) bpy.ops.object.mode_set(mode="EDIT") ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_axis(context)) diff --git a/src/bonsai/bonsai/bim/module/style/data.py b/src/bonsai/bonsai/bim/module/style/data.py index afcc7eba8d..09cb5447b1 100644 --- a/src/bonsai/bonsai/bim/module/style/data.py +++ b/src/bonsai/bonsai/bim/module/style/data.py @@ -97,9 +97,8 @@ class BlenderMaterialStyleData: material = obj.active_material if not material: return False - props = material.BIMStyleProperties - style_id = props.ifc_definition_id - style = tool.Ifc.get_entity_by_id(style_id) + + style = tool.Ifc.get_entity(material) if not style: return False diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index 6ec0ccdc3c..0ead9b6751 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -118,8 +118,7 @@ class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator): # Don't check blender_material and style_id as this operator is only called from UI. assert isinstance(self.blender_material, str) # Type checker. material = bpy.data.materials[self.blender_material] - style_id = material.BIMStyleProperties.ifc_definition_id - style = tool.Ifc.get_entity_by_id(style_id) + style = tool.Ifc.get_entity(material) # Material is linked to a style from a different project. if not style or tool.Ifc.get_object(style) != material: @@ -221,18 +220,25 @@ class UpdateCurrentStyle(bpy.types.Operator): def execute(self, context): style = tool.Ifc.get().by_id(self.style_id) material = tool.Ifc.get_object(style) - current_style_type = material.BIMStyleProperties.active_style_type + msprops = tool.Style.get_material_style_props(material) + current_style_type = msprops.active_style_type if self.update_all: sprops = tool.Style.get_style_props() sprops.active_style_type = current_style_type return {"FINISHED"} - updated_materials = set() + updated_materials: set[bpy.types.Material] = set() for obj in context.selected_objects: + if not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve)): + continue for mat in obj.data.materials: - if mat and mat not in updated_materials and mat.BIMStyleProperties.ifc_definition_id != 0: - mat.BIMStyleProperties.active_style_type = current_style_type + if ( + mat + and mat not in updated_materials + and (msprops_ := tool.Style.get_material_style_props(mat)).ifc_definition_id != 0 + ): + msprops_.active_style_type = current_style_type updated_materials.add(mat) return {"FINISHED"} @@ -755,7 +761,8 @@ class EnableEditingSurfaceStyle(bpy.types.Operator): bonsai.bim.helper.import_attributes2(surface_style or self.ifc_class, attributes, callback) material = tool.Ifc.get_object(style) - active_style_type = material.BIMStyleProperties.active_style_type + msprops = tool.Style.get_material_style_props(material) + active_style_type = msprops.active_style_type if self.ifc_class == "IfcExternallyDefinedSurfaceStyle" and active_style_type != "External": if tool.Style.has_blender_external_style(style_elements): tool.Style.switch_shading(material, "External") @@ -803,9 +810,10 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): # restore selected style type material = tool.Ifc.get_object(self.style) - material.BIMStyleProperties.active_style_type = material.BIMStyleProperties.active_style_type + msprops = tool.Style.get_material_style_props(material) + msprops.active_style_type = msprops.active_style_type - def edit_existing_style(self): + def edit_existing_style(self) -> None: ifc_file = tool.Ifc.get() material = tool.Ifc.get_object(self.style) assert self.surface_style @@ -867,7 +875,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): attributes=bonsai.bim.helper.export_attributes(attributes), ) - def add_new_style(self): + def add_new_style(self) -> None: material = tool.Ifc.get_object(self.style) if self.props.is_editing_class == "IfcSurfaceStyleShading": surface_style = ifcopenshell.api.run( @@ -911,13 +919,13 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): attributes=bonsai.bim.helper.export_attributes(attributes), ) - def get_shading_attributes(self): + def get_shading_attributes(self) -> dict[str, Any]: return { "SurfaceColour": self.color_to_dict(self.props.surface_colour), "Transparency": self.props.transparency or None, } - def get_rendering_attributes(self): + def get_rendering_attributes(self) -> dict[str, Any]: if self.props.is_diffuse_colour_null: diffuse_colour = None elif self.props.diffuse_colour_class == "IfcColourRgb": @@ -961,7 +969,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): textures.append(texture_data) return textures - def color_to_dict(self, x): + def color_to_dict(self, x: tuple[float, float, float]) -> dict[str, Any]: return {"Red": x[0], "Green": x[1], "Blue": x[2]} diff --git a/src/bonsai/bonsai/bim/module/style/prop.py b/src/bonsai/bonsai/bim/module/style/prop.py index 040db82f60..d533473ba2 100644 --- a/src/bonsai/bonsai/bim/module/style/prop.py +++ b/src/bonsai/bonsai/bim/module/style/prop.py @@ -39,13 +39,13 @@ from typing import Literal, Union, TYPE_CHECKING, get_args _ = gettext.gettext -def get_style_types(self, context): +def get_style_types(self: "BIMStylesProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not StylesData.is_loaded: StylesData.load() return StylesData.data["style_types"] -def get_reflectance_methods(self, context): +def get_reflectance_methods(self: "BIMStylesProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not StylesData.is_loaded: StylesData.load() return StylesData.data["reflectance_methods"] @@ -77,6 +77,16 @@ class Style(PropertyGroup): type=bpy.types.Material, ) + if TYPE_CHECKING: + ifc_definition_id: int + total_elements: int + style_classes: bpy.types.bpy_prop_collection_idprop[StrProperty] + has_surface_colour: bool + surface_colour: tuple[float, float, float] + has_diffuse_colour: bool + diffuse_colour: tuple[float, float, float] + blender_material: Union[bpy.types.Material, None] + STYLE_TYPES = [ ("Shading", "Shading", ""), @@ -86,7 +96,7 @@ STYLE_TYPES = [ def update_shading_styles(self: "BIMStylesProperties", context: bpy.types.Context) -> None: for mat in bpy.data.materials: - if mat.BIMStyleProperties.ifc_definition_id == 0: + if tool.Blender.get_ifc_definition_id(mat) == 0: continue tool.Style.change_current_style_type(mat, self.active_style_type) @@ -111,7 +121,9 @@ UV_MODES = [ ("Camera", "Camera", _("UV from position coordinate in camera space")), ] - +TextureMapMode = Literal[ + "DIFFUSE", "NORMAL", "METALLICROUGHNESS", "SPECULAR", "SHININESS", "EMISSIVE", "OCCLUSION", "AMBIENT" +] TEXTURE_MAPS_MODS = ( ("DIFFUSE", "DIFFUSE", ""), ("NORMAL", "NORMAL", ""), @@ -129,6 +141,10 @@ class Texture(PropertyGroup): # NOTE: subtype `FILE_PATH` is not used to avoid .blend relative paths path: StringProperty(name="Texture Path", update=update_shader_graph) + if TYPE_CHECKING: + mode: TextureMapMode + path: str + class ColourRgb(PropertyGroup): name: StringProperty() @@ -136,6 +152,10 @@ class ColourRgb(PropertyGroup): # not exposed in the UI, here just to preserve the data color_name: StringProperty(name="Color Name") + if TYPE_CHECKING: + color_value: tuple[float, float, float] + color_name: str + # to fit blender.bim.helper.export_attributes def get_value(self): return { diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py index 4cdf8aa88d..a5042fa800 100644 --- a/src/bonsai/bonsai/bim/module/style/ui.py +++ b/src/bonsai/bonsai/bim/module/style/ui.py @@ -16,11 +16,16 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.bim.helper import bonsai.tool as tool from bpy.types import Panel, UIList from bonsai.bim.module.style.data import StylesData, BlenderMaterialStyleData +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.style.prop import BIMStylesProperties, Style class BIM_PT_styles(Panel): @@ -94,7 +99,8 @@ class BIM_PT_styles(Panel): if active_style: row = self.layout.row(align=True) if material := style.blender_material: - row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="") + msprops = tool.Style.get_material_style_props(material) + row.prop(msprops, "active_style_type", icon="SHADING_RENDERED", text="") op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="") op.style_id = style.ifc_definition_id @@ -255,11 +261,19 @@ class BIM_PT_styles(Panel): class BIM_UL_styles(UIList): - def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_property): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMStylesProperties, + item: Style, + icon, + active_data, + active_property, + ): if item: row = layout.row(align=True) - props = tool.Style.get_style_props() - if item.ifc_definition_id == props.is_editing_style: + if item.ifc_definition_id == data.is_editing_style: row.label(text="", icon="GREASEPENCIL") row.prop(item, "name", text="", emboss=False) if item.has_surface_colour: @@ -295,7 +309,7 @@ class BIM_PT_style(Panel): @classmethod def poll(cls, context): - return bool(tool.Ifc.get() and (material := context.material) and material.BIMStyleProperties.ifc_definition_id) + return bool(tool.Ifc.get() and (material := context.material) and tool.Blender.get_ifc_definition_id(material)) def draw(self, context): # NOTE: this UI is needed only to indicate whether blender material is linked to IFC @@ -305,10 +319,11 @@ class BIM_PT_style(Panel): BlenderMaterialStyleData.load() material = context.material - style_id = material.BIMStyleProperties.ifc_definition_id + assert material + style_id = tool.Blender.get_ifc_definition_id(material) row = self.layout.row(align=True) - if style_id and not BlenderMaterialStyleData.data["is_linked_to_style"]: + if not BlenderMaterialStyleData.data["is_linked_to_style"]: row.label(text="Material has linked IFC from a different project.") op = row.operator("bim.unlink_style", icon="UNLINKED", text="") op.blender_material = material.name diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 6ea77101a1..c8846ace19 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1601,5 +1601,7 @@ class Blender(bonsai.core.tool.Blender): return obj.BIMObjectProperties @classmethod - def get_ifc_definition_id(cls, obj: bpy.types.Object) -> int: - return tool.Blender.get_object_bim_props(obj).ifc_definition_id + def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int: + if isinstance(obj, bpy.types.Object): + return tool.Blender.get_object_bim_props(obj).ifc_definition_id + return tool.Style.get_material_style_props(obj).ifc_definition_id diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 56c8e275c6..9524e91ea3 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -639,7 +639,7 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_object_materials_without_styles(cls, obj: bpy.types.Object) -> list[bpy.types.Material]: return [ - s.material for s in obj.material_slots if s.material and not s.material.BIMStyleProperties.ifc_definition_id + s.material for s in obj.material_slots if s.material and not tool.Blender.get_ifc_definition_id(s.material) ] @classmethod diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 5f68fc4b0f..83c05101ef 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -110,7 +110,7 @@ class Ifc(bonsai.core.tool.Ifc): if isinstance(obj, bpy.types.Object): props = tool.Blender.get_object_bim_props(obj) elif isinstance(obj, bpy.types.Material): - props = obj.BIMStyleProperties + props = tool.Style.get_material_style_props(obj) else: props = tool.Geometry.get_mesh_props(obj) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 2af631b671..0704fe7154 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -293,8 +293,19 @@ class Model(bonsai.core.tool.Model): else: break + unit_scale: float + vertices: list[Vector] + edges: list[Sequence[int]] + arcs: list[Sequence[int]] + circles: list[Sequence[int]] + @classmethod - def import_axis(cls, axis, obj=None, position=None): + def import_axis( + cls, + axis: Union[ifcopenshell.entity_instance, tuple[Vector, Vector]], + obj=None, + position: Optional[Matrix] = None, + ) -> bpy.types.Object: cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if position is None: @@ -305,7 +316,7 @@ class Model(bonsai.core.tool.Model): cls.arcs = [] cls.circles = [] - if isinstance(axis, list): + if isinstance(axis, tuple): cls.vertices.extend( [ position @ Vector(cls.convert_unit_to_si(axis[0])).to_3d(), @@ -478,7 +489,7 @@ class Model(bonsai.core.tool.Model): @classmethod def convert_curve_to_mesh( cls, - obj: bpy.types.Object, + obj: Union[bpy.types.Object, None], # Unused argument. position: Matrix, curve: ifcopenshell.entity_instance, x_angle: Optional[float] = None, @@ -1624,7 +1635,7 @@ class Model(bonsai.core.tool.Model): loop_edges = list(bm.edges) # Create loops from edges - loops = [] + loops: list[list[bmesh.types.BMEdge]] = [] while loop_edges: edge = loop_edges.pop() loop = [edge] @@ -1645,19 +1656,19 @@ class Model(bonsai.core.tool.Model): tmp = ifcopenshell.file(schema=tool.Ifc.get().schema) - def is_in_group(v, group_name): + def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool: for group_index in groups[group_name]: if group_index in v[deform_layer]: return True return False - def get_group_index(v, group_name): + def get_group_index(v: bmesh.types.BMVert, group_name: str) -> Union[int, None]: for group_index in groups[group_name]: if group_index in v[deform_layer]: return group_index # Convert all loops into IFC curves - curves = [] + curves: list[ifcopenshell.entity_instance] = [] for loop in loops: if len(loop) == 1 and all([is_in_group(v, "IFCCIRCLE") for v in loop[0].verts]): @@ -1670,7 +1681,7 @@ class Model(bonsai.core.tool.Model): tmp.createIfcCircle(tmp.createIfcAxis2Placement2D(tmp.createIfcCartesianPoint(list(mid))), radius) ) else: - loop_verts = [] + loop_verts: list[bmesh.types.BMVert] = [] for i, edge in enumerate(loop): if i == 0 and len(loop) == 1: loop_verts.append(edge.verts[0]) @@ -1746,7 +1757,7 @@ class Model(bonsai.core.tool.Model): curves.append(tmp.createIfcIndexedPolyCurve(points)) # Sort IFC curves into either closed, or closed with void profile defs - profile_defs = [] + profile_defs: list[ifcopenshell.entity_instance] = [] settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 4c824f8b62..b8269f4134 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -418,8 +418,8 @@ class Root(bonsai.core.tool.Root): """Rename material without triggerring name callback and unnecessary writing to IFC.""" if material.name == name: return - props = material.BIMStyleProperties - props.is_renaming = True + msprops = tool.Style.get_material_style_props(material) + msprops.is_renaming = True material.name = name # The handler will trigger, and reset is_renaming to False. @classmethod diff --git a/src/bonsai/bonsai/tool/style.py b/src/bonsai/bonsai/tool/style.py index b7a7cb7241..42a3dc207a 100644 --- a/src/bonsai/bonsai/tool/style.py +++ b/src/bonsai/bonsai/tool/style.py @@ -162,14 +162,14 @@ class Style(bonsai.core.tool.Style): cls, blender_material_or_style: Union[bpy.types.Material, ifcopenshell.entity_instance] ) -> dict[str, ifcopenshell.entity_instance]: if isinstance(blender_material_or_style, bpy.types.Material): - if not (ifc_definition_id := blender_material_or_style.BIMStyleProperties.ifc_definition_id): + style = tool.Ifc.get_entity(blender_material_or_style) + if not style: return {} - style = tool.Ifc.get().by_id(ifc_definition_id) else: style = blender_material_or_style style_elements = {} - for style in style.Styles: - style_elements[style.is_a()] = style + for style_ in style.Styles: + style_elements[style_.is_a()] = style_ return style_elements @classmethod @@ -305,7 +305,7 @@ class Style(bonsai.core.tool.Style): return next((l.from_node for l in input_pin.links if l.from_node.type == of_type), None) return next((l.from_node for l in input_pin.links), None) - props = obj.BIMStyleProperties + props = tool.Style.get_material_style_props(obj) transparency = 1 - obj.diffuse_color[3] diffuse_color = obj.diffuse_color viewport_color = color_to_ifc_format(obj.diffuse_color) @@ -487,16 +487,14 @@ class Style(bonsai.core.tool.Style): @classmethod def get_surface_shading_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]: - if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id: - style = tool.Ifc.get().by_id(ifc_definition_id) + if style := tool.Ifc.get_entity(obj): items = [s for s in style.Styles if s.is_a() == "IfcSurfaceStyleShading"] if items: return items[0] @classmethod def get_surface_texture_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]: - if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id: - style = tool.Ifc.get().by_id(ifc_definition_id) + if style := tool.Ifc.get_entity(obj): items = [s for s in style.Styles if s.is_a("IfcSurfaceStyleWithTextures")] if items: return items[0] @@ -581,7 +579,8 @@ class Style(bonsai.core.tool.Style): @classmethod def change_current_style_type(cls, blender_material: bpy.types.Material, style_type: str) -> None: - blender_material.BIMStyleProperties.active_style_type = style_type + props = cls.get_material_style_props(blender_material) + props.active_style_type = style_type @classmethod def get_styled_items(cls, style: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: @@ -632,7 +631,8 @@ class Style(bonsai.core.tool.Style): @classmethod def reload_material_from_ifc(cls, blender_material: bpy.types.Material) -> None: - blender_material.BIMStyleProperties.active_style_type = blender_material.BIMStyleProperties.active_style_type + props = cls.get_material_style_props(blender_material) + props.active_style_type = props.active_style_type @classmethod def switch_shading(cls, blender_material: bpy.types.Material, style_type: StyleType) -> None: diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 2163489685..7385543ff9 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -911,14 +911,14 @@ def the_material_name_is_not_an_ifc_material(name): @then(parsers.parse('the material "{name}" is an IFC style')) def the_material_name_is_an_ifc_style(name): obj = the_material_name_exists(name) - ifc_definition_id = obj.BIMStyleProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id != 0, f"The material {obj} has a style ID of {ifc_definition_id}" @then(parsers.parse('the material "{name}" is not an IFC style')) def the_material_name_is_not_an_ifc_style(name): obj = the_material_name_exists(name) - ifc_definition_id = obj.BIMStyleProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id == 0, f"The material {obj} has a style ID of {ifc_definition_id}" diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 2a2cb6b718..6c4c338b3d 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -891,11 +891,12 @@ class TestAddReferenceImage(NewFile): assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0))) material = obj.active_material + assert material assert material.name == "image" - assert material.BIMStyleProperties.ifc_definition_id != 0 + assert tool.Blender.get_ifc_definition_id(material) != 0 - ifc_file = tool.Ifc.get() - style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id) + style = tool.Ifc.get_entity(material) + assert style styled_items = set(tool.Style.get_styled_items(style)) representation_items = set(tool.Geometry.get_active_representation(obj).Items) assert styled_items == representation_items diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py index c0d1819d9a..85a0a4c79d 100644 --- a/src/bonsai/test/tool/test_geometry.py +++ b/src/bonsai/test/tool/test_geometry.py @@ -112,7 +112,8 @@ class TestGetObjectMaterialsWithoutStyles(NewFile): material1 = bpy.data.materials.new("Material") material2 = bpy.data.materials.new("Material") material3 = bpy.data.materials.new("Material") - material3.BIMStyleProperties.ifc_definition_id = 1 + props = tool.Style.get_material_style_props(material3) + props.ifc_definition_id = 1 obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) obj.data.materials.append(material1) obj.data.materials.append(material2) @@ -307,7 +308,7 @@ class TestRecordObjectMaterials(NewFile): tool.Ifc.set(ifc) style = ifc.createIfcSurfaceStyle() material = bpy.data.materials.new("Material") - material.BIMStyleProperties.ifc_definition_id = style.id() + tool.Ifc.link(style, material) obj.data.materials.append(material) subject.record_object_materials(obj) assert tool.Geometry.get_mesh_props(obj.data).material_checksum == str([style.id()]) diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index afcf0e6b5a..8056db1349 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -462,7 +462,7 @@ class TestApplyIfcMaterialChanges(NewFile): def get_used_styles(self, obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]: ifc_file = tool.Ifc.get() return { - ifc_file.by_id(s.material.BIMStyleProperties.ifc_definition_id) for s in obj.material_slots if s.material + ifc_file.by_id(tool.Blender.get_ifc_definition_id(s.material)) for s in obj.material_slots if s.material } def get_mesh(self, obj: bpy.types.Object) -> bpy.types.Mesh: diff --git a/src/bonsai/test/tool/test_polyline.py b/src/bonsai/test/tool/test_polyline.py index b7d4303b0d..9dd1e8f41c 100644 --- a/src/bonsai/test/tool/test_polyline.py +++ b/src/bonsai/test/tool/test_polyline.py @@ -18,6 +18,9 @@ import bpy import ifcopenshell +import ifcopenshell.api.project +import ifcopenshell.api.root +import ifcopenshell.api.unit import bonsai.core.tool import bonsai.tool as tool from test.bim.bootstrap import NewFile diff --git a/src/bonsai/test/tool/test_style.py b/src/bonsai/test/tool/test_style.py index 6af83895a3..5f37d397b5 100644 --- a/src/bonsai/test/tool/test_style.py +++ b/src/bonsai/test/tool/test_style.py @@ -308,7 +308,8 @@ class TestGetSurfaceRenderingStyle(NewFile): style_item = tool.Ifc.get().createIfcSurfaceStyleRendering() style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item]) obj = bpy.data.materials.new("Material") - obj.BIMStyleProperties.ifc_definition_id = style.id() + props = tool.Style.get_material_style_props(obj) + props.ifc_definition_id = style.id() assert subject.get_surface_rendering_style(obj) == style_item @@ -347,7 +348,7 @@ class TestGetSurfaceShadingStyle(NewFile): style_item = tool.Ifc.get().createIfcSurfaceStyleShading() style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item]) obj = bpy.data.materials.new("Material") - obj.BIMStyleProperties.ifc_definition_id = style.id() + tool.Ifc.link(style, obj) assert subject.get_surface_shading_style(obj) == style_item def test_do_not_get_rendering_styles(self): @@ -355,7 +356,7 @@ class TestGetSurfaceShadingStyle(NewFile): style_item = tool.Ifc.get().createIfcSurfaceStyleRendering() style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item]) obj = bpy.data.materials.new("Material") - obj.BIMStyleProperties.ifc_definition_id = style.id() + tool.Ifc.link(style, obj) assert subject.get_surface_shading_style(obj) is None @@ -365,7 +366,7 @@ class TestGetSurfaceTextureStyle(NewFile): style_item = tool.Ifc.get().createIfcSurfaceStyleWithTextures() style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item]) obj = bpy.data.materials.new("Material") - obj.BIMStyleProperties.ifc_definition_id = style.id() + tool.Ifc.link(style, obj) assert subject.get_surface_texture_style(obj) == style_item diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index ddaab44e20..9458131046 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -21,159 +21,203 @@ import ifcopenshell.util.element import ifcopenshell.util.shape import ifcopenshell.util.unit from ifcopenshell.util.data import Clipping -from typing import Any, Union, Optional, Literal +from typing import Any, Union, Optional, Literal, get_args VECTOR_3D = tuple[float, float, float] +CardinalPointNumeric = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] +CardinalPointString = Literal[ + "bottom left", + "bottom centre", + "bottom right", + "mid-depth left", + "mid-depth centre", + "mid-depth right", + "top left", + "top centre", + "top right", + "geometric centroid", + "bottom in line with the geometric centroid", + "left in line with the geometric centroid", + "right in line with the geometric centroid", + "top in line with the geometric centroid", + "shear centre", + "bottom in line with the shear centre", + "left in line with the shear centre", + "right in line with the shear centre", + "top in line with the shear centre", +] +CARDINAL_POINT_VALUES: tuple[CardinalPointString, ...] = get_args(CardinalPointString) +CardinalPoint = Union[CardinalPointNumeric, CardinalPointString] def add_profile_representation( file: ifcopenshell.file, - # IfcGeometricRepresentationContext context: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance, - # in meters depth: float = 1.0, - cardinal_point: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 5, - # A list of planes that define clipping half space solids - # Planes are defined either by Clipping objects - # or by dictionaries of arguments for `Clipping.parse` + # TODO: None makes more sense as default value? + cardinal_point: Union[CardinalPoint, None] = 5, clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None, placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None), ) -> ifcopenshell.entity_instance: + """Add profile representation. + + :param context: The IfcGeometricRepresentationContext for the representation, + only Model/Body/MODEL_VIEW type of representations are currently supported. + :param profile: The IfcProfileDef to extrude. + :param depth: The depth of the extrusion in meters. + :param cardinal_point: The cardinal point of the profile. + :param clippings: A list of planes that define clipping half space solids. + Planes are defined either by Clipping objects + or by dictionaries of arguments for `Clipping.parse`. + :param placement_zx_axes: A tuple of two vectors that define the placement of the profile. + The first vector is the Z axis, the second vector is the X axis. + :return: IfcShapeRepresentation. + """ usecase = Usecase() usecase.file = file - usecase.settings = { - "context": context, - "profile": profile, - "depth": depth, - "cardinal_point": cardinal_point, - "clippings": clippings if clippings is not None else [], - "placement_zx_axes": placement_zx_axes, - } - return usecase.execute() + clippings = clippings if clippings is not None else [] + return usecase.execute(context, profile, depth, cardinal_point, clippings, placement_zx_axes) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] + clippings: list[Clipping] - def execute(self): - self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]] - return self.file.createIfcShapeRepresentation( - self.settings["context"], - self.settings["context"].ContextIdentifier, - "Clipping" if self.settings["clippings"] else "SweptSolid", + def execute( + self, + context: ifcopenshell.entity_instance, + profile: ifcopenshell.entity_instance, + depth: float, + cardinal_point: Union[CardinalPoint, None], + clippings: list[Union[Clipping, dict[str, Any]]], + placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]], + ) -> ifcopenshell.entity_instance: + if isinstance(cardinal_point, int): + cardinal_point = CARDINAL_POINT_VALUES[cardinal_point - 1] + + self.cardinal_point = cardinal_point + self.profile = profile + self.clippings = [Clipping.parse(c) for c in clippings] + self.depth = depth + self.placement_zx_axes = placement_zx_axes + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + return self.file.create_entity( + "IfcShapeRepresentation", + context, + context.ContextIdentifier, + "Clipping" if self.clippings else "SweptSolid", [self.create_item()], ) - def create_item(self): + def create_item(self) -> ifcopenshell.entity_instance: point = self.get_point() placement = self.file.createIfcAxis2Placement3D( point, - self.file.createIfcDirection(self.settings["placement_zx_axes"][0] or (0.0, 0.0, 1.0)), - self.file.createIfcDirection(self.settings["placement_zx_axes"][1] or (1.0, 0.0, 0.0)), + self.file.create_entity("IfcDirection", self.placement_zx_axes[0] or (0.0, 0.0, 1.0)), + self.file.create_entity("IfcDirection", self.placement_zx_axes[1] or (1.0, 0.0, 0.0)), ) - extrusion = self.file.createIfcExtrudedAreaSolid( - self.settings["profile"], + extrusion = self.file.create_entity( + "IfcExtrudedAreaSolid", + self.profile, placement, self.file.createIfcDirection((0.0, 0.0, 1.0)), - self.convert_si_to_unit(self.settings["depth"]), + self.convert_si_to_unit(self.depth), ) - if self.settings["clippings"]: + if self.clippings: return self.apply_clippings(extrusion) return extrusion - def apply_clippings(self, first_operand): - while self.settings["clippings"]: - clipping = self.settings["clippings"].pop() + def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + while self.clippings: + clipping = self.clippings.pop() if isinstance(clipping, ifcopenshell.entity_instance): new = ifcopenshell.util.element.copy(self.file, clipping) new.FirstOperand = first_operand first_operand = new else: # Clipping - first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"]) + first_operand = clipping.apply(self.file, first_operand, self.unit_scale) return first_operand - def convert_si_to_unit(self, co): - return co / self.settings["unit_scale"] + def convert_si_to_unit(self, co: float) -> float: + return co / self.unit_scale - def get_point(self): - if not self.settings["cardinal_point"]: + def get_point(self) -> ifcopenshell.entity_instance: + if not self.cardinal_point: return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) - elif self.settings["cardinal_point"] == 1: + elif self.cardinal_point == "bottom left": return self.file.createIfcCartesianPoint((-self.get_x() / 2, self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 2: + elif self.cardinal_point == "bottom centre": return self.file.createIfcCartesianPoint((0.0, self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 3: + elif self.cardinal_point == "bottom right": return self.file.createIfcCartesianPoint((self.get_x() / 2, self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 4: + elif self.cardinal_point == "mid-depth left": return self.file.createIfcCartesianPoint((-self.get_x() / 2, 0.0, 0.0)) - elif self.settings["cardinal_point"] == 5: + elif self.cardinal_point == "mid-depth centre": return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) - elif self.settings["cardinal_point"] == 6: + elif self.cardinal_point == "mid-depth right": return self.file.createIfcCartesianPoint((self.get_x() / 2, 0.0, 0.0)) - elif self.settings["cardinal_point"] == 7: + elif self.cardinal_point == "top left": return self.file.createIfcCartesianPoint((-self.get_x() / 2, -self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 8: + elif self.cardinal_point == "top centre": return self.file.createIfcCartesianPoint((0.0, -self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 9: + elif self.cardinal_point == "top right": return self.file.createIfcCartesianPoint((self.get_x() / 2, -self.get_y() / 2, 0.0)) # TODO other cardinal points return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) - def get_x(self): - if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"): - return self.settings["profile"].OverallWidth - elif self.settings["profile"].is_a("IfcCShapeProfileDef"): - return self.settings["profile"].Width - elif self.settings["profile"].is_a("IfcCircleProfileDef"): - return self.settings["profile"].Radius * 2 - elif self.settings["profile"].is_a("IfcEllipseProfileDef"): - return self.settings["profile"].SemiAxis1 * 2 - elif self.settings["profile"].is_a("IfcIShapeProfileDef"): - return self.settings["profile"].OverallWidth - elif self.settings["profile"].is_a("IfcLShapeProfileDef"): - return self.settings["profile"].Width - elif self.settings["profile"].is_a("IfcRectangleProfileDef"): - return self.settings["profile"].XDim - elif self.settings["profile"].is_a("IfcTShapeProfileDef"): - return self.settings["profile"].FlangeWidth - elif self.settings["profile"].is_a("IfcUShapeProfileDef"): - return self.settings["profile"].FlangeWidth - elif self.settings["profile"].is_a("IfcZShapeProfileDef"): - return (self.settings["profile"].FlangeWidth * 2) - self.settings["profile"].WebThickness + def get_x(self) -> float: + if self.profile.is_a("IfcAsymmetricIShapeProfileDef"): + return self.profile.OverallWidth + elif self.profile.is_a("IfcCShapeProfileDef"): + return self.profile.Width + elif self.profile.is_a("IfcCircleProfileDef"): + return self.profile.Radius * 2 + elif self.profile.is_a("IfcEllipseProfileDef"): + return self.profile.SemiAxis1 * 2 + elif self.profile.is_a("IfcIShapeProfileDef"): + return self.profile.OverallWidth + elif self.profile.is_a("IfcLShapeProfileDef"): + return self.profile.Width + elif self.profile.is_a("IfcRectangleProfileDef"): + return self.profile.XDim + elif self.profile.is_a("IfcTShapeProfileDef"): + return self.profile.FlangeWidth + elif self.profile.is_a("IfcUShapeProfileDef"): + return self.profile.FlangeWidth + elif self.profile.is_a("IfcZShapeProfileDef"): + return (self.profile.FlangeWidth * 2) - self.profile.WebThickness else: settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"]) + shape = ifcopenshell.geom.create_shape(settings, self.profile) return self.convert_si_to_unit(ifcopenshell.util.shape.get_x(shape)) return 0.0 - def get_y(self): - if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"): - return self.settings["profile"].OverallDepth - elif self.settings["profile"].is_a("IfcCShapeProfileDef"): - return self.settings["profile"].Depth - elif self.settings["profile"].is_a("IfcCircleProfileDef"): - return self.settings["profile"].Radius * 2 - elif self.settings["profile"].is_a("IfcEllipseProfileDef"): - return self.settings["profile"].SemiAxis2 * 2 - elif self.settings["profile"].is_a("IfcIShapeProfileDef"): - return self.settings["profile"].OverallDepth - elif self.settings["profile"].is_a("IfcLShapeProfileDef"): - return self.settings["profile"].Depth - elif self.settings["profile"].is_a("IfcRectangleProfileDef"): - return self.settings["profile"].YDim - elif self.settings["profile"].is_a("IfcTShapeProfileDef"): - return self.settings["profile"].Depth - elif self.settings["profile"].is_a("IfcUShapeProfileDef"): - return self.settings["profile"].Depth - elif self.settings["profile"].is_a("IfcZShapeProfileDef"): - return self.settings["profile"].Depth + def get_y(self) -> float: + if self.profile.is_a("IfcAsymmetricIShapeProfileDef"): + return self.profile.OverallDepth + elif self.profile.is_a("IfcCShapeProfileDef"): + return self.profile.Depth + elif self.profile.is_a("IfcCircleProfileDef"): + return self.profile.Radius * 2 + elif self.profile.is_a("IfcEllipseProfileDef"): + return self.profile.SemiAxis2 * 2 + elif self.profile.is_a("IfcIShapeProfileDef"): + return self.profile.OverallDepth + elif self.profile.is_a("IfcLShapeProfileDef"): + return self.profile.Depth + elif self.profile.is_a("IfcRectangleProfileDef"): + return self.profile.YDim + elif self.profile.is_a("IfcTShapeProfileDef"): + return self.profile.Depth + elif self.profile.is_a("IfcUShapeProfileDef"): + return self.profile.Depth + elif self.profile.is_a("IfcZShapeProfileDef"): + return self.profile.Depth else: settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"]) + shape = ifcopenshell.geom.create_shape(settings, self.profile) return self.convert_si_to_unit(ifcopenshell.util.shape.get_y(shape)) return 0.0 diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index 0fc5cccdf9..4c004ecfdd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -149,6 +149,8 @@ class Usecase: self.settings_2d.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) shape = ifcopenshell.geom.create_shape(self.settings_2d, dummy_solid) + # NOTE: points do not need unit conversion + # as dummy file is inherently using project units. if self.cardinal_point == 1: return self.get_bottom_left(shape) elif self.cardinal_point == 2: diff --git a/src/ifcopenshell-python/ifcopenshell/util/data.py b/src/ifcopenshell-python/ifcopenshell/util/data.py index 365f0df2cb..9f02d973a5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/data.py +++ b/src/ifcopenshell-python/ifcopenshell/util/data.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations import numpy as np import ifcopenshell from typing import Any, Union @@ -30,7 +31,9 @@ class Clipping: operand_type: str = "IfcHalfSpaceSolid" @classmethod - def parse(cls, raw_data: Any) -> Union[ifcopenshell.entity_instance, "Clipping", None]: + def parse( + cls, raw_data: Union[ifcopenshell.entity_instance, Clipping, dict[str, Any]] + ) -> Union[ifcopenshell.entity_instance, Clipping]: """Parse various formats into a clipping object `raw_data` can be either: diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py b/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py index 1ee1b3ac87..c827f1e33a 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py +++ b/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py @@ -20,6 +20,7 @@ import test.bootstrap import ifcopenshell.api.root import ifcopenshell.api.context import ifcopenshell.api.geometry +import ifcopenshell.util.shape_builder class TestAddShapeAspect(test.bootstrap.IFC4):