Merge pull request #8342 from falken10vdl/style-flat-pretty-toggle

Style flat pretty toggle
This commit is contained in:
falken10vdl
2026-07-24 09:56:24 +02:00
committed by GitHub
9 changed files with 721 additions and 17 deletions
+4 -3
View File
@@ -418,9 +418,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():
-4
View File
@@ -133,10 +133,6 @@ class MaterialCreator:
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"]:
@@ -1299,6 +1299,10 @@ class LoadProjectElements(bpy.types.Operator):
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]:
@@ -45,7 +45,9 @@ classes = (
operator.SelectByStyle,
operator.SelectStyleInStylesUI,
operator.SetAssetMaterialToExternalStyle,
operator.SuggestShadeFromExternalStyle,
operator.UnlinkStyle,
operator.TogglePreferIfcShading,
operator.UpdateCurrentStyle,
operator.UpdateStyleColours,
operator.UpdateStyleTextures,
+227 -8
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import colorsys
import os
from pathlib import Path
from typing import Any, Union
@@ -238,11 +239,13 @@ class UpdateCurrentStyle(bpy.types.Operator):
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 (msprops_ := tool.Style.get_material_style_props(mat)).ifc_definition_id != 0
):
if not mat:
continue
msprops_ = tool.Style.get_material_style_props(mat)
if msprops_.ifc_definition_id == 0:
continue
if mat in updated_materials:
continue
msprops_.active_style_type = current_style_type
updated_materials.add(mat)
return {"FINISHED"}
@@ -457,10 +460,14 @@ class ActivateExternalStyle(bpy.types.Operator):
self.report({"ERROR"}, f"Error loading external style for \"{material.name}\" - {db['msg']}")
return {"CANCELLED"}
self.copy_material_attributes(db["data_block"], material)
ext_mat = db["data_block"]
self.copy_material_attributes(ext_mat, material)
if tool.Style.get_use_nodes(material):
tool.Blender.copy_node_graph(material, db["data_block"])
bpy.data.materials.remove(db["data_block"])
if material.get("bim_dual_branch"):
tool.Style.update_external_branch(material, ext_mat)
else:
tool.Style.setup_dual_branch(material, ext_mat)
bpy.data.materials.remove(ext_mat)
return {"FINISHED"}
def copy_material_attributes(self, source, target):
@@ -503,6 +510,218 @@ class ActivateExternalStyle(bpy.types.Operator):
set_prop(prop_name)
class TogglePreferIfcShading(bpy.types.Operator):
bl_idname = "bim.toggle_prefer_ifc_shading"
bl_label = "Toggle Flat/Pretty"
bl_description = (
"Toggle between Flat (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 tool.Style.get_material_style_props(source_mat).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)]
for mat in ifc_mats:
tool.Style.get_material_style_props(mat).prefer_ifc_shading = new_value
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"})
value_offset: bpy.props.FloatProperty(
name="Value",
description="Offset added to the colour's value (-1 = fully dark, 0 = unchanged, +1 = fully light)",
default=0.0,
min=-1.0,
max=1.0,
step=1,
precision=2,
options={"SKIP_SAVE"},
)
saturation_factor: bpy.props.FloatProperty(
name="Saturation",
description="Scale applied to the colour's saturation (0 = greyscale, 1 = unchanged, >1 = more saturated)",
default=1.0,
min=0.0,
max=2.0,
step=1,
precision=2,
options={"SKIP_SAVE"},
)
def invoke(self, context, event):
if event.alt:
self.all_styles = True
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "value_offset", slider=True)
layout.prop(self, "saturation_factor", slider=True)
if self.all_styles:
layout.label(text="Will apply to all external styles", icon="INFO")
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
color_cache: dict[tuple[str, str, str], tuple | None] = {}
try:
for i, (mat, style_elements) in enumerate(candidates):
wm.progress_update(i)
if self._apply_to_material(
mat, style_elements, self.value_offset, self.saturation_factor, color_cache
):
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, self.value_offset, self.saturation_factor)
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,
value_offset: float = 0.0,
saturation_factor: float = 1.0,
color_cache: "dict[tuple[str, str, str], tuple | None] | None" = None,
) -> 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("/")
cache_key = (str(style_path), data_block_type, data_block)
if color_cache is not None and cache_key in color_cache:
cached = color_cache[cache_key]
if cached is None:
return False # previously failed for this path
surface_colour, transparency = cached
else:
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}')
if color_cache is not None:
color_cache[cache_key] = None
return False
if not db["data_block"]:
self.report({"WARNING"}, f'Could not load external style for "{material.name}": {db["msg"]}')
if color_cache is not None:
color_cache[cache_key] = None
return False
ext_mat = db["data_block"]
surface_colour, transparency = tool.Style.get_representative_material_color(ext_mat)
bpy.data.materials.remove(ext_mat)
if color_cache is not None:
color_cache[cache_key] = (surface_colour, transparency)
if value_offset != 0.0 or saturation_factor != 1.0:
h, s, v = colorsys.rgb_to_hsv(*surface_colour)
v = max(0.0, min(1.0, v + value_offset))
s = max(0.0, min(1.0, s * saturation_factor))
surface_colour = colorsys.hsv_to_rgb(h, s, v)
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,
)
material.diffuse_color = (*surface_colour, 1.0 - transparency)
tool.Style.sync_flat_branch_shading(material, surface_colour, transparency)
return True
class DisableEditingStyles(bpy.types.Operator):
bl_idname = "bim.disable_editing_styles"
bl_options = {"REGISTER", "UNDO"}
@@ -372,6 +372,16 @@ 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:
style_elements = tool.Style.get_style_elements(self.id_data)
has_external = tool.Style.has_blender_external_style(style_elements)
if self.prefer_ifc_shading or not has_external:
self.active_style_type = "Shading"
else:
self.active_style_type = "External"
self.id_data.update_tag()
class BIMStyleProperties(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
active_style_type: EnumProperty(
@@ -381,9 +391,19 @@ class BIMStyleProperties(PropertyGroup):
default="Shading",
update=update_shading_style,
)
prefer_ifc_shading: BoolProperty(
name="Flat / Pretty",
description=(
"Toggle between Flat (IFC-native shading) and Pretty (external .blend style). "
"When set to Flat, viewport switches to Material Preview or Rendered will not activate the external style."
),
default=False,
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
+65
View File
@@ -110,6 +110,11 @@ class BIM_PT_styles(Panel):
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
op.style_id = 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)
if self.props.style_type == "IfcSurfaceStyle":
self.layout.label(text="Surface Style Element:")
col = self.layout.column(align=True)
@@ -161,6 +166,66 @@ class BIM_PT_styles(Panel):
edit_label = "Save Lighting Style"
self.draw_edit_ui(edit_label)
def draw_style_status_row(self, material: bpy.types.Material, msprops) -> 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_flat = space.shading.color_type != "TEXTURE"
mode_label = "Flat"
dep_label = "Shade"
if not is_flat:
mode_label = "Pretty"
dep_label = "Texture \u2192 Shade"
elif shading_type in ("MATERIAL", "RENDERED"):
is_flat = msprops.prefer_ifc_shading
mode_label = "Flat"
dep_label = "Render+Texture \u2192 Render \u2192 Shade"
if not is_flat:
mode_label = "Pretty"
dep_label = "External \u2192 Render+Texture \u2192 Render \u2192 Shade"
else:
is_flat = False
mode_label = ""
dep_label = ""
row1 = box.row(align=True)
row1.label(text=" | ".join(parts))
if mode_label:
row2 = box.row(align=True)
row2.label(text=f"Current Mode: {mode_label} \u2014 {dep_label}")
row3 = box.row(align=True)
row3.alignment = "RIGHT"
op = row3.operator("bim.suggest_shade_from_external_style", text="", icon="BRUSHES_ALL")
op.material_name = material.name
op = row3.operator("bim.toggle_prefer_ifc_shading", text="", icon="UV_SYNC_SELECT")
op.material_name = material.name
def draw_surface_style_shading(self):
row = self.layout.row()
row.prop(self.props, "surface_colour")
+34
View File
@@ -803,6 +803,40 @@ class Blender(bonsai.core.tool.Blender):
# restore shader editor settings
shader_editor.pin = previous_pin_setting
@classmethod
def copy_node_graph_additive(
cls, material_to: bpy.types.Material, material_from: bpy.types.Material
) -> bpy.types.ShaderNodeOutputMaterial | None:
"""Paste nodes from material_from alongside the existing nodes in material_to.
Unlike copy_node_graph this does NOT clear the existing node tree first.
Returns the OUTPUT_MATERIAL node that was added from material_from, or None.
"""
temp_override = cls.get_shader_editor_context()
shader_editor = temp_override["space"]
before_names = {n.name for n in material_to.node_tree.nodes}
previous_pin_setting = shader_editor.pin
shader_editor.pin = True
shader_editor.node_tree = material_from.node_tree
for node in material_from.node_tree.nodes:
node.select = True
with bpy.context.temp_override(**temp_override):
bpy.ops.node.clipboard_copy()
shader_editor.node_tree = material_to.node_tree
with bpy.context.temp_override(**temp_override):
bpy.ops.node.clipboard_paste(offset=(0, 0))
shader_editor.pin = previous_pin_setting
for node in material_to.node_tree.nodes:
if node.name not in before_names and node.type == "OUTPUT_MATERIAL":
return node
return None
@classmethod
def get_material_node(
cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {}
+363
View File
@@ -597,6 +597,146 @@ class Style(bonsai.core.tool.Style):
external_style = style_elements.get("IfcExternallyDefinedSurfaceStyle", None)
return bool(external_style and external_style.Location and external_style.Location.endswith(".blend"))
@classmethod
def _color_from_principled(cls, node: bpy.types.Node) -> tuple[tuple[float, float, float], float]:
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
@classmethod
def _color_from_shader_socket(
cls, socket: bpy.types.NodeSocket, seen: set[str] | None = None
) -> tuple[tuple[float, float, float], float] | 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 == "BSDF_PRINCIPLED":
return cls._color_from_principled(node)
if node.type in ("BSDF_DIFFUSE", "DIFFUSE_BSDF"):
return cls._resolve_color_socket(node.inputs["Color"]), 0.0
if node.type == "BSDF_GLASS":
return cls._resolve_color_socket(node.inputs["Color"]), 0.0
if node.type in ("MIX_SHADER", "ADD_SHADER"):
for inp in node.inputs:
if inp.type == "SHADER" and inp.is_linked:
result = cls._color_from_shader_socket(inp, seen)
if result:
return result
return None
@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
output_node = next(
(n for n in nodes if n.type == "OUTPUT_MATERIAL" and n.is_active_output), None
) or next((n for n in nodes if n.type == "OUTPUT_MATERIAL"), None)
if output_node:
result = cls._color_from_shader_socket(output_node.inputs["Surface"])
if result:
return result
# Fallback: scan all shader nodes if no output node or graph traversal found nothing
for node in nodes:
if node.type == "BSDF_PRINCIPLED":
return cls._color_from_principled(node)
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 _collect_upstream_sources(cls, socket: bpy.types.NodeSocket, seen: set[str]) -> list[tuple[str, object]]:
"""Recursively collect all upstream colour/image sources reachable from *socket*."""
results = []
for link in socket.links:
node = link.from_node
if node.name in seen:
continue
seen.add(node.name)
if node.type == "TEX_IMAGE":
results.append(("IMAGE", node.image))
elif node.type == "VALTORGB":
results.append(("COLORRAMP", node))
else:
for inp in node.inputs:
if inp.is_linked:
results.extend(cls._collect_upstream_sources(inp, seen))
return results
@classmethod
def _upstream_color_source(
cls, socket: bpy.types.NodeSocket, seen: set[str] | None = None
) -> tuple[str, object] | None:
sources = cls._collect_upstream_sources(socket, set() if seen is None else seen)
# Prefer a concrete image texture over a colour ramp (which may be greyscale/procedural).
for s in sources:
if s[0] == "IMAGE":
return s
for s in sources:
if s[0] == "COLORRAMP":
return s
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()
@@ -674,9 +814,179 @@ class Style(bonsai.core.tool.Style):
props = cls.get_material_style_props(blender_material)
props.active_style_type = props.active_style_type
@classmethod
def get_branch_outputs(
cls, material: bpy.types.Material
) -> tuple[bpy.types.ShaderNode | None, bpy.types.ShaderNode | None]:
"""Return (external_output_node, flat_output_node), or (None, None) if not dual-branch."""
if not material.node_tree:
return None, None
ext = material.node_tree.nodes.get("BIM_Output_External")
fast = material.node_tree.nodes.get("BIM_Output_Flat")
return ext, fast
@classmethod
def _remove_external_branch(cls, material: bpy.types.Material) -> None:
"""Remove all nodes reachable from BIM_Output_External (walks links backwards)."""
if not material.node_tree:
return
nodes = material.node_tree.nodes
output = nodes.get("BIM_Output_External")
if not output:
return
to_remove: set[str] = set()
stack = [output]
while stack:
node = stack.pop()
if node.name in to_remove:
continue
to_remove.add(node.name)
for inp in node.inputs:
for link in inp.links:
stack.append(link.from_node)
for name in list(to_remove):
n = nodes.get(name)
if n:
nodes.remove(n)
@classmethod
def _build_flat_branch_nodes(cls, material: bpy.types.Material) -> bpy.types.ShaderNode:
"""Add a Principled BSDF flat-branch to material's existing node tree.
Reads IfcSurfaceStyleRendering or IfcSurfaceStyleShading from the linked IFC entity.
Defaults to a white BSDF when no IFC shading data is available.
Returns the new Material Output node (named BIM_Output_Flat, is_active_output=False).
"""
from mathutils import Vector
style_elements = cls.get_style_elements(material)
nodes = material.node_tree.nodes
links = material.node_tree.links
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = Vector((10, -600))
output = nodes.new("ShaderNodeOutputMaterial")
output.name = "BIM_Output_Flat"
output.location = Vector((300, -600))
output.is_active_output = False
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
rendering_style = None
shading_only = None
for surface_style in style_elements.values():
if surface_style.is_a() == "IfcSurfaceStyleShading":
shading_only = surface_style
elif surface_style.is_a("IfcSurfaceStyleRendering"):
rendering_style = surface_style
shading_only = None
if rendering_style:
d = tool.Loader.surface_style_to_dict(rendering_style)
if d.get("DiffuseColour"):
ctype, cval = d["DiffuseColour"]
if ctype == "IfcColourRgb":
bsdf.inputs["Base Color"].default_value = cval + (1,)
solid_color = cval
else:
cval = tuple(v * cval for v in d["SurfaceColour"])
bsdf.inputs["Base Color"].default_value = cval + (1,)
solid_color = cval
else:
r, g, b = d["SurfaceColour"]
bsdf.inputs["Base Color"].default_value = (r, g, b, 1.0)
solid_color = (r, g, b)
if d.get("SpecularColour"):
ctype, cval = d["SpecularColour"]
if ctype == "IfcNormalisedRatioMeasure":
bsdf.inputs["Metallic"].default_value = cval
if d.get("SpecularHighlight"):
bsdf.inputs["Roughness"].default_value = d["SpecularHighlight"]
transparency = d.get("Transparency") or 0.0
bsdf.inputs["Alpha"].default_value = 1 - transparency
if transparency > 0:
material.blend_method = "BLEND"
material.diffuse_color = solid_color + (1.0 - transparency,)
elif shading_only:
d = tool.Loader.surface_style_to_dict(shading_only)
r, g, b = d["SurfaceColour"]
alpha = 1 - (d.get("Transparency") or 0.0)
bsdf.inputs["Base Color"].default_value = (r, g, b, 1.0)
bsdf.inputs["Alpha"].default_value = alpha
if alpha < 1.0:
material.blend_method = "BLEND"
material.diffuse_color = (r, g, b, alpha)
# else: leave default white Principled BSDF
return output
@classmethod
def setup_dual_branch(cls, material: bpy.types.Material, ext_material: bpy.types.Material) -> bool:
"""Build a dual-branch node tree: flat branch from IFC data + external branch from ext_material.
Clears any existing nodes and builds both branches from scratch.
External branch output (BIM_Output_External) is set active Pretty mode.
Flat branch output (BIM_Output_Flat) is inactive Flat mode.
Returns True on success; False if no shader editor is available (falls back to single-branch).
"""
cls.set_use_nodes(material, True)
for n in material.node_tree.nodes[:]:
material.node_tree.nodes.remove(n)
cls._build_flat_branch_nodes(material)
ext_output = tool.Blender.copy_node_graph_additive(material, ext_material)
if not ext_output:
# No shader editor available: fall back to single-branch
tool.Blender.copy_node_graph(material, ext_material)
return False
ext_output.name = "BIM_Output_External"
ext_output.is_active_output = True
material["bim_dual_branch"] = True
return True
@classmethod
def update_external_branch(cls, material: bpy.types.Material, ext_material: bpy.types.Material) -> None:
"""Replace the external-branch nodes of an already dual-branch material."""
cls._remove_external_branch(material)
ext_output = tool.Blender.copy_node_graph_additive(material, ext_material)
if ext_output:
ext_output.name = "BIM_Output_External"
ext_output.is_active_output = True
fast = material.node_tree.nodes.get("BIM_Output_Flat")
if fast:
fast.is_active_output = False
@classmethod
def sync_flat_branch_shading(
cls, material: bpy.types.Material, surface_colour: tuple[float, float, float], transparency: float
) -> None:
"""Update the flat-branch Principled BSDF with new shading values.
Call this after creating or editing IfcSurfaceStyleShading so the flat branch
stays in sync without requiring a full setup_dual_branch rebuild.
"""
if not material.node_tree:
return
fast_output = material.node_tree.nodes.get("BIM_Output_Flat")
if not fast_output:
return
for link in fast_output.inputs["Surface"].links:
if link.from_node.type == "BSDF_PRINCIPLED":
bsdf = link.from_node
r, g, b = surface_colour
bsdf.inputs["Base Color"].default_value = (r, g, b, 1.0)
bsdf.inputs["Alpha"].default_value = 1.0 - transparency
break
@classmethod
def switch_shading(cls, blender_material: bpy.types.Material, style_type: StyleType) -> None:
if style_type == "External":
ext, fast = cls.get_branch_outputs(blender_material)
if ext and fast:
ext.is_active_output = True
fast.is_active_output = False
blender_material.update_tag()
return
try:
bpy.ops.bim.activate_external_style(material_name=blender_material.name)
except RuntimeError as error:
@@ -684,21 +994,41 @@ class Style(bonsai.core.tool.Style):
return
raise error
elif style_type == "Shading":
ext, fast = cls.get_branch_outputs(blender_material)
if ext and fast:
fast.is_active_output = True
ext.is_active_output = False
blender_material.update_tag()
return
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}"
@@ -744,3 +1074,36 @@ class Style(bonsai.core.tool.Style):
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)
_last_shading_type: str | None = None
@classmethod
def restore_material_style_types(cls, shading_type: str) -> None:
"""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.
"""
if cls._last_shading_type == shading_type:
return
cls._last_shading_type = shading_type
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 shading_type == "SOLID":
props.active_style_type = "Shading"
shading = style_elements.get("IfcSurfaceStyleRendering") or style_elements.get("IfcSurfaceStyleShading")
if shading:
d = tool.Loader.surface_style_to_dict(shading)
alpha = 1.0 - (d.get("Transparency") or 0.0)
material.diffuse_color = d["SurfaceColour"] + (alpha,)
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"
material.update_tag()