diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index e11eb07ce8f..fbd94d877e1 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -335,9 +335,10 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None def viewport_shading_changed_callback(area: bpy.types.Area) -> None: - shading = area.spaces.active.shading.type - if shading == "RENDERED": - tool.Style.get_style_props().active_style_type = "External" + shading_type = area.spaces.active.shading.type + tool.Style.restore_material_style_types(shading_type) + if shading_type == "SOLID": + area.spaces.active.shading.color_type = "MATERIAL" def subscribe_to_viewport_shading_changes(): diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 267800469a2..39c90e4b291 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -133,10 +133,6 @@ def load_texture_maps(self, shape_has_openings: bool) -> None: if shape_has_openings and coords.is_a("IfcIndexedTextureMap"): continue tool.Loader.load_indexed_map(coords, self.mesh) - elif tool.Style.get_texture_style(material): - # No explicit coordinate mapping (e.g. IFC2X3 has no IsMappedBy, - # and IFC4 COORD uses generated UVs). Bake XY→UV as fallback. - tool.Loader.load_generated_uv_map(self.mesh) def assign_material_slots_to_faces(self) -> None: if not self.mesh["ios_materials"]: diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index e24f6f41d85..6ebe9635ff7 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1239,6 +1239,10 @@ def execute(self, context): tool.Project.set_default_modeling_dimensions() tool.Root.reload_grid_decorator() bonsai.bim.handler.refresh_ui_data() + for screen in bpy.data.screens: + for area in screen.areas: + if area.type == "VIEW_3D": + bonsai.bim.handler.viewport_shading_changed_callback(area) return {"FINISHED"} def get_decomposition_elements(self) -> set[ifcopenshell.entity_instance]: diff --git a/src/bonsai/bonsai/bim/module/style/__init__.py b/src/bonsai/bonsai/bim/module/style/__init__.py index 6f13fd6f2d6..3102435160d 100644 --- a/src/bonsai/bonsai/bim/module/style/__init__.py +++ b/src/bonsai/bonsai/bim/module/style/__init__.py @@ -45,7 +45,9 @@ operator.SelectByStyle, operator.SelectStyleInStylesUI, operator.SetAssetMaterialToExternalStyle, + operator.SuggestShadeFromExternalStyle, operator.UnlinkStyle, + operator.TogglePreferIfcShading, operator.UpdateCurrentStyle, operator.UpdateStyleColours, operator.UpdateStyleTextures, diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index e7d1d05e0ab..5ecec6f640f 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -503,6 +503,165 @@ def set_prop_group(prop_group): set_prop(prop_name) +class TogglePreferIfcShading(bpy.types.Operator): + bl_idname = "bim.toggle_prefer_ifc_shading" + bl_label = "Toggle Fast/Pretty" + bl_description = ( + "Toggle between Fast (IFC-native shading) and Pretty (external .blend style) for ALL styles.\n\n" + "SHIFT+CLICK to apply to this style only" + ) + bl_options = {"REGISTER", "UNDO"} + material_name: bpy.props.StringProperty(name="Material Name", default="", options={"SKIP_SAVE"}) + single_only: bpy.props.BoolProperty(name="Single Only", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + if event.shift: + self.single_only = True + return self.execute(context) + + def execute(self, context): + wm = context.window_manager + space = tool.Blender.get_view3d_space() + is_solid = space and space.shading.type == "SOLID" + + if is_solid: + if space.shading.color_type == "TEXTURE": + space.shading.color_type = "MATERIAL" + else: + meshes_needing_uv = [] + for obj in bpy.context.scene.objects: + if not isinstance(obj.data, bpy.types.Mesh): + continue + for slot in obj.material_slots: + mat = slot.material + if not mat or not tool.Blender.get_ifc_definition_id(mat): + continue + style_elements = tool.Style.get_style_elements(mat) + if style_elements.get("IfcSurfaceStyleWithTextures") and not obj.data.uv_layers: + meshes_needing_uv.append(obj.data) + break + wm.progress_begin(0, max(len(meshes_needing_uv), 1)) + try: + for i, mesh in enumerate(meshes_needing_uv): + tool.Loader.load_generated_uv_map(mesh) + wm.progress_update(i) + finally: + wm.progress_end() + space.shading.color_type = "TEXTURE" + return {"FINISHED"} + + if self.single_only: + mat = bpy.data.materials.get(self.material_name) + if not mat: + return {"CANCELLED"} + msprops = tool.Style.get_material_style_props(mat) + msprops.prefer_ifc_shading = not msprops.prefer_ifc_shading + else: + # Default: apply to all IFC materials + source_mat = bpy.data.materials.get(self.material_name) + new_value = not source_mat.BIMStyleProperties.prefer_ifc_shading if source_mat else True + ifc_mats = [m for m in bpy.data.materials if tool.Blender.get_ifc_definition_id(m)] + wm.progress_begin(0, max(len(ifc_mats), 1)) + try: + for i, mat in enumerate(ifc_mats): + tool.Style.get_material_style_props(mat).prefer_ifc_shading = new_value + wm.progress_update(i) + finally: + wm.progress_end() + return {"FINISHED"} + + +class SuggestShadeFromExternalStyle(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.suggest_shade_from_external_style" + bl_label = "Suggest Shade from External Style" + bl_description = ( + "Generate a Shade style (Surface Colour + Transparency) from the external .blend style.\n\n" + "ALT+CLICK to apply to all styles with an external .blend style" + ) + bl_options = {"REGISTER", "UNDO"} + material_name: bpy.props.StringProperty(name="Material Name", default="", options={"SKIP_SAVE"}) + all_styles: bpy.props.BoolProperty(name="All Styles", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + if event.alt: + self.all_styles = True + return self.execute(context) + + def _execute(self, context): + if self.all_styles: + candidates = [ + (mat, tool.Style.get_style_elements(mat)) + for mat in bpy.data.materials + if tool.Blender.get_ifc_definition_id(mat) + ] + candidates = [(mat, se) for mat, se in candidates if tool.Style.has_blender_external_style(se)] + wm = context.window_manager + wm.progress_begin(0, max(len(candidates), 1)) + count = 0 + try: + for i, (mat, style_elements) in enumerate(candidates): + wm.progress_update(i) + if self._apply_to_material(mat, style_elements): + count += 1 + finally: + wm.progress_end() + self.report({"INFO"}, f"Shade style generated for {count} style(s).") + else: + mat = bpy.data.materials.get(self.material_name) + if not mat: + return {"CANCELLED"} + style_elements = tool.Style.get_style_elements(mat) + if not tool.Style.has_blender_external_style(style_elements): + self.report({"ERROR"}, "No external .blend style assigned. Please assign an external style first.") + return {"CANCELLED"} + self._apply_to_material(mat, style_elements) + props = tool.Style.get_style_props() + if props.is_editing: + core.load_styles(tool.Style, style_type=props.style_type) + + def _apply_to_material(self, material: bpy.types.Material, style_elements: dict) -> bool: + external_style = style_elements["IfcExternallyDefinedSurfaceStyle"] + style_path = Path(tool.Ifc.resolve_uri(external_style.Location)) + data_block_type, data_block = external_style.Identification.split("/") + + try: + db = tool.Blender.append_data_block(str(style_path), data_block_type, data_block) + except OSError as e: + self.report({"WARNING"}, f'Could not open blend file for "{material.name}": {e}') + return False + if not db["data_block"]: + self.report({"WARNING"}, f'Could not load external style for "{material.name}": {db["msg"]}') + return False + + ext_mat = db["data_block"] + surface_colour, transparency = tool.Style.get_representative_material_color(ext_mat) + bpy.data.materials.remove(ext_mat) + + ifc_style = tool.Ifc.get_entity(material) + attributes: dict = { + "SurfaceColour": { + "Name": None, + "Red": surface_colour[0], + "Green": surface_colour[1], + "Blue": surface_colour[2], + }, + } + if tool.Ifc.get_schema() != "IFC2X3": + attributes["Transparency"] = transparency + + shading_style = style_elements.get("IfcSurfaceStyleShading") + if shading_style: + tool.Ifc.run("style.edit_surface_style", style=shading_style, attributes=attributes) + else: + tool.Ifc.run( + "style.add_surface_style", + style=ifc_style, + ifc_class="IfcSurfaceStyleShading", + attributes=attributes, + ) + return True + + class DisableEditingStyles(bpy.types.Operator): bl_idname = "bim.disable_editing_styles" bl_options = {"REGISTER", "UNDO"} diff --git a/src/bonsai/bonsai/bim/module/style/prop.py b/src/bonsai/bonsai/bim/module/style/prop.py index bd62021d653..48603d64de9 100644 --- a/src/bonsai/bonsai/bim/module/style/prop.py +++ b/src/bonsai/bonsai/bim/module/style/prop.py @@ -118,6 +118,19 @@ def update_shader_graph(self: Union["Texture", "BIMStylesProperties"], context: tool.Loader.create_surface_style_with_textures(material, shading_data, textures_data) +def _make_clear_null_updater(null_prop: str): + def _update(self: "BIMStylesProperties", context: bpy.types.Context) -> None: + self[null_prop] = False + update_shader_graph(self, context) + + return _update + + +update_diffuse_colour = _make_clear_null_updater("is_diffuse_colour_null") +update_specular_colour = _make_clear_null_updater("is_specular_colour_null") +update_specular_highlight_value = _make_clear_null_updater("is_specular_highlight_null") + + UV_MODES = [ ("UV", "UV", _("Actual UV data presented on the geometry")), ("Generated", "Generated", _("Automatically-generated UV from the vertex positions of the mesh")), @@ -221,24 +234,29 @@ class BIMStylesProperties(PropertyGroup): transparency: bpy.props.FloatProperty( name="Transparency", default=0.0, min=0.0, max=1.0, update=update_shader_graph ) - # TODO: do something on null? - is_diffuse_colour_null: BoolProperty(name="Is Null") + is_diffuse_colour_null: BoolProperty(name="Is Null", update=update_shader_graph) diffuse_colour_class: EnumProperty( items=[(x, x, "") for x in get_args(ColourClass)], name="Diffuse Colour Class", - update=update_shader_graph, + update=update_diffuse_colour, ) diffuse_colour: bpy.props.FloatVectorProperty( - name="Diffuse Colour", subtype="COLOR", default=(1, 1, 1), min=0.0, max=1.0, size=3, update=update_shader_graph + name="Diffuse Colour", + subtype="COLOR", + default=(1, 1, 1), + min=0.0, + max=1.0, + size=3, + update=update_diffuse_colour, ) diffuse_colour_ratio: bpy.props.FloatProperty( - name="Diffuse Ratio", default=0.0, min=0.0, max=1.0, update=update_shader_graph + name="Diffuse Ratio", default=0.0, min=0.0, max=1.0, update=update_diffuse_colour ) - is_specular_colour_null: BoolProperty(name="Is Null") + is_specular_colour_null: BoolProperty(name="Is Null", update=update_shader_graph) specular_colour_class: EnumProperty( items=[(x, x, "") for x in get_args(ColourClass)], name="Specular Colour Class", - update=update_shader_graph, + update=update_specular_colour, default="IfcNormalisedRatioMeasure", ) specular_colour: bpy.props.FloatVectorProperty( @@ -248,7 +266,7 @@ class BIMStylesProperties(PropertyGroup): min=0.0, max=1.0, size=3, - update=update_shader_graph, + update=update_specular_colour, ) specular_colour_ratio: bpy.props.FloatProperty( name="Specular Ratio", @@ -256,16 +274,16 @@ class BIMStylesProperties(PropertyGroup): default=0.0, min=0.0, max=1.0, - update=update_shader_graph, + update=update_specular_colour, ) - is_specular_highlight_null: BoolProperty(name="Is Null") + is_specular_highlight_null: BoolProperty(name="Is Null", update=update_shader_graph) specular_highlight: bpy.props.FloatProperty( name="Specular Highlight", description="Used as Roughness value in PHYSICAL Reflectance Method", default=0.0, min=0.0, max=1.0, - update=update_shader_graph, + update=update_specular_highlight_value, ) reflectance_method: EnumProperty( name="Reflectance Method", @@ -347,6 +365,22 @@ def update_shading_style(self: "BIMStyleProperties", context: bpy.types.Context) tool.Style.switch_shading(blender_material, self.active_style_type) +def update_prefer_ifc_shading(self: "BIMStyleProperties", context: bpy.types.Context) -> None: + blender_material = self.id_data + style_elements = tool.Style.get_style_elements(blender_material) + has_external = tool.Style.has_blender_external_style(style_elements) + if self.prefer_ifc_shading: + self.active_style_type = "Shading" + tool.Style.switch_shading(blender_material, "Shading") + elif has_external: + self.active_style_type = "External" + tool.Style.switch_shading(blender_material, "External") + else: + self.active_style_type = "Shading" + tool.Style.switch_shading(blender_material, "Shading") + blender_material.update_tag() + + class BIMStyleProperties(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") active_style_type: EnumProperty( @@ -356,9 +390,19 @@ class BIMStyleProperties(PropertyGroup): default="Shading", update=update_shading_style, ) + prefer_ifc_shading: BoolProperty( + name="Fast / Pretty", + description=( + "Toggle between Fast (IFC-native shading) and Pretty (external .blend style). " + "When set to Fast, viewport switches to Material Preview or Rendered will not activate the external style." + ), + default=True, + update=update_prefer_ifc_shading, + ) is_renaming: BoolProperty(description="Used to prevent triggering handler callback.", default=False) if TYPE_CHECKING: ifc_definition_id: int active_style_type: tool.Style.StyleType + prefer_ifc_shading: bool is_renaming: bool diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py index 54ca4f5aa48..92bcae7b043 100644 --- a/src/bonsai/bonsai/bim/module/style/ui.py +++ b/src/bonsai/bonsai/bim/module/style/ui.py @@ -81,6 +81,11 @@ def draw(self, context): op.style = style.ifc_definition_id row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id + if active_style and self.props.style_type == "IfcSurfaceStyle": + if material := style.blender_material: + msprops = tool.Style.get_material_style_props(material) + self.draw_style_status_row(material, msprops) + self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index") # adding a new IfcSurfaceStyle @@ -102,13 +107,6 @@ def draw(self, context): # style ui tools if active_style: - row = self.layout.row(align=True) - if material := style.blender_material: - 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 - if self.props.style_type == "IfcSurfaceStyle": self.layout.label(text="Surface Style Element:") col = self.layout.column(align=True) @@ -160,6 +158,83 @@ def draw(self, context): edit_label = "Save Lighting Style" self.draw_edit_ui(edit_label) + def draw_style_status_row(self, material: bpy.types.Material, msprops: "BIMStyleProperties") -> None: + space = tool.Blender.get_view3d_space() + box = self.layout.box() + + obj = bpy.context.active_object + + parts = [] + if space: + shading_type = space.shading.type + shading_labels = { + "SOLID": "Solid", + "MATERIAL": "Material Preview", + "RENDERED": "Rendered", + "WIREFRAME": "Wireframe", + } + parts.append(f"Viewport: {shading_labels.get(shading_type, shading_type)}") + else: + parts.append("No 3D viewport") + shading_type = None + + if obj: + obj_has_uv = isinstance(obj.data, bpy.types.Mesh) and bool(obj.data.uv_layers) + uv_label = "UV \u2713" if obj_has_uv else "UV \u2717" + parts.append(f"Selected Object: {obj.name} {uv_label}") + else: + parts.append("Selected Object: None") + + if shading_type == "SOLID": + is_fast = space.shading.color_type != "TEXTURE" + if is_fast: + parts.append("Fast: Shade") + else: + parts.append("Pretty: Texture \u2192 Shade") + elif shading_type in ("MATERIAL", "RENDERED"): + is_fast = msprops.prefer_ifc_shading + if is_fast: + parts.append("Fast: Render+Texture \u2192 Render \u2192 Shade") + else: + parts.append("Pretty: External \u2192 Render+Texture \u2192 Render \u2192 Shade") + + row1 = box.row(align=True) + row1.label(text=" | ".join(parts)) + + row2 = box.row(align=True) + row2.alignment = "RIGHT" + op = row2.operator("bim.suggest_shade_from_external_style", text="", icon="BRUSHES_ALL") + op.material_name = material.name + if shading_type == "SOLID": + is_fast = space.shading.color_type != "TEXTURE" + shading_text = "Fast" if is_fast else "Pretty" + op = row2.operator("bim.toggle_prefer_ifc_shading", text=shading_text, depress=is_fast) + else: + shading_text = "Fast" if msprops.prefer_ifc_shading else "Pretty" + op = row2.operator("bim.toggle_prefer_ifc_shading", text=shading_text, depress=msprops.prefer_ifc_shading) + op.material_name = material.name + + @staticmethod + def _get_shader_label(material: bpy.types.Material, msprops: "BIMStyleProperties") -> str: + space = tool.Blender.get_view3d_space() + if space and space.shading.type == "SOLID": + return "Not Applicable" + if msprops.active_style_type == "External": + return "External (.blend)" + if not material.node_tree: + return "Flat colour" + nodes = material.node_tree.nodes + has_mix = any(n.type == "MIX_SHADER" for n in nodes) + if has_mix: + return "Emission (Flat)" + has_principled = any(n.type == "BSDF_PRINCIPLED" for n in nodes) + has_teximage = any(n.type == "TEX_IMAGE" and any(o.links for o in n.outputs) for n in nodes) + if has_principled and has_teximage: + return "BSDF + Textures" + if has_principled: + return "Principled BSDF" + return "Flat colour" + def draw_surface_style_shading(self): row = self.layout.row() row.prop(self.props, "surface_colour") diff --git a/src/bonsai/bonsai/tool/style.py b/src/bonsai/bonsai/tool/style.py index 8db3ed30fee..ee326b71a34 100644 --- a/src/bonsai/bonsai/tool/style.py +++ b/src/bonsai/bonsai/tool/style.py @@ -203,6 +203,11 @@ def get_shading_style_data_from_props(cls) -> dict[str, Any]: available_props = props.bl_rna.properties.keys() for prop_blender, prop_ifc in STYLE_PROPS_MAP.items(): + null_prop_name = f"is_{prop_blender}_null" + if null_prop_name in available_props and getattr(props, null_prop_name): + surface_style_data[prop_ifc] = None + continue + class_prop_name = f"{prop_blender}_class" # get detailed color properties if available @@ -587,6 +592,98 @@ def has_blender_external_style(cls, style_elements: dict[str, ifcopenshell.entit external_style = style_elements.get("IfcExternallyDefinedSurfaceStyle", None) return bool(external_style and external_style.Location and external_style.Location.endswith(".blend")) + @classmethod + def get_representative_material_color( + cls, material: bpy.types.Material + ) -> tuple[tuple[float, float, float], float]: + if material.node_tree: + nodes = material.node_tree.nodes + for node in nodes: + if node.type == "BSDF_PRINCIPLED": + color = cls._resolve_color_socket(node.inputs["Base Color"]) + alpha_socket = node.inputs["Alpha"] + alpha_source = cls._upstream_color_source(alpha_socket) + if alpha_source and alpha_source[0] == "IMAGE": + pixels = alpha_source[1].pixels[:] + n = len(pixels) // 4 + step = max(1, n // 4096) + a_sum = sum(pixels[i * 4 + 3] for i in range(0, n, step)) + count = len(range(0, n, step)) or 1 + transparency = 1.0 - (a_sum / count) + else: + transparency = 1.0 - alpha_socket.default_value + return color, transparency + for node in nodes: + if node.type in ("BSDF_DIFFUSE", "DIFFUSE_BSDF"): + return cls._resolve_color_socket(node.inputs["Color"]), 0.0 + for node in nodes: + if node.type == "BSDF_GLASS": + return cls._resolve_color_socket(node.inputs["Color"]), 0.0 + color = tuple(material.diffuse_color[:3]) + transparency = 1.0 - material.diffuse_color[3] + return color, transparency + + @classmethod + def _upstream_color_source( + cls, socket: bpy.types.NodeSocket, seen: set[str] | None = None + ) -> tuple[str, object] | None: + if seen is None: + seen = set() + for link in socket.links: + node = link.from_node + if node.name in seen: + continue + seen.add(node.name) + if node.type == "TEX_IMAGE": + return ("IMAGE", node.image) + if node.type == "VALTORGB": + return ("COLORRAMP", node) + for inp in node.inputs: + if inp.is_linked: + result = cls._upstream_color_source(inp, seen) + if result: + return result + return None + + @classmethod + def _resolve_color_socket(cls, socket: bpy.types.NodeSocket) -> tuple[float, float, float]: + source = cls._upstream_color_source(socket) + if source is None: + return tuple(socket.default_value[:3]) + kind, obj = source + if kind == "IMAGE": + return cls._average_image_color(obj) + if kind == "COLORRAMP": + return cls._average_colorramp_color(obj) + return tuple(socket.default_value[:3]) + + @staticmethod + def _average_image_color(image: bpy.types.Image) -> tuple[float, float, float]: + pixels = image.pixels[:] + n = len(pixels) // 4 + if n == 0: + return (0.5, 0.5, 0.5) + step = max(1, n // 4096) + r_sum = g_sum = b_sum = 0.0 + count = 0 + for i in range(0, n, step): + base = i * 4 + r_sum += pixels[base] + g_sum += pixels[base + 1] + b_sum += pixels[base + 2] + count += 1 + return (r_sum / count, g_sum / count, b_sum / count) + + @staticmethod + def _average_colorramp_color(node: bpy.types.Node) -> tuple[float, float, float]: + elements = node.color_ramp.elements + if not elements: + return (0.5, 0.5, 0.5) + r = sum(e.color[0] for e in elements) / len(elements) + g = sum(e.color[1] for e in elements) / len(elements) + b = sum(e.color[2] for e in elements) / len(elements) + return (r, g, b) + @classmethod def is_editing_styles(cls) -> bool: props = cls.get_style_props() @@ -677,18 +774,32 @@ def switch_shading(cls, blender_material: bpy.types.Material, style_type: StyleT style_elements = tool.Style.get_style_elements(blender_material) rendering_style = None texture_style = None + shading_only_style = None for surface_style in style_elements.values(): if surface_style.is_a() == "IfcSurfaceStyleShading": + shading_only_style = surface_style tool.Loader.create_surface_style_shading(blender_material, surface_style) elif surface_style.is_a("IfcSurfaceStyleRendering"): rendering_style = surface_style + shading_only_style = None # rendering overrides shading-only path tool.Loader.create_surface_style_rendering(blender_material, surface_style) elif surface_style.is_a("IfcSurfaceStyleWithTextures"): texture_style = surface_style if rendering_style and texture_style: tool.Loader.create_surface_style_with_textures(blender_material, rendering_style, texture_style) + elif shading_only_style and not rendering_style: + # create a minimal Principled BSDF so Material Preview/Rendered shows the colour instead of white. + tool.Style.set_use_nodes(blender_material, True) + tool.Loader.restart_material_node_tree(blender_material) + bsdf = tool.Blender.get_material_node(blender_material, "BSDF_PRINCIPLED") + if bsdf: + r, g, b, a = blender_material.diffuse_color + bsdf.inputs["Base Color"].default_value = (r, g, b, 1) + bsdf.inputs["Alpha"].default_value = a + if a < 1.0: + blender_material.blend_method = "BLEND" else: assert False, f"Invalid style type found: {style_type}" @@ -734,3 +845,45 @@ def reload_representations(cls, style: ifcopenshell.entity_instance) -> None: elements = ifcopenshell.util.element.get_elements_by_style(tool.Ifc.get(), style) objects = [tool.Ifc.get_object(e) for e in elements] tool.Geometry.reload_representation(objects) + + @classmethod + def restore_material_style_types(cls, shading_type: str) -> bool: + """Set each IFC material's active_style_type to the richest available for the given viewport mode. + + In SOLID mode all materials use "Shading". + In MATERIAL_PREVIEW / RENDERED, materials with an external .blend style use "External" + unless prefer_ifc_shading is set on that material. + + Returns True if any IFC material has IfcSurfaceStyleWithTextures (used to decide color_type). + """ + has_any_textures = False + for material in bpy.data.materials: + if not tool.Blender.get_ifc_definition_id(material): + continue + props = cls.get_material_style_props(material) + style_elements = cls.get_style_elements(material) + if style_elements.get("IfcSurfaceStyleWithTextures"): + has_any_textures = True + if shading_type == "SOLID": + props.active_style_type = "Shading" + else: # MATERIAL_PREVIEW or RENDERED + if cls.has_blender_external_style(style_elements) and not props.prefer_ifc_shading: + props.active_style_type = "External" + else: + props.active_style_type = "Shading" + return has_any_textures + + @classmethod + def ensure_uv_maps_for_textured_objects(cls) -> None: + """Generate UV maps for all mesh objects whose IFC material has IfcSurfaceStyleWithTextures.""" + for obj in bpy.context.scene.objects: + if not isinstance(obj.data, bpy.types.Mesh): + continue + for slot in obj.material_slots: + material = slot.material + if not material or not tool.Blender.get_ifc_definition_id(material): + continue + style_elements = cls.get_style_elements(material) + if style_elements.get("IfcSurfaceStyleWithTextures") and not obj.data.uv_layers: + tool.Loader.load_generated_uv_map(obj.data) + break