mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
Add Flat/Pretty style toggle and dual-branch external style management
(cherry picked from commit c77a28c862)
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,13 +239,15 @@ 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
|
||||
):
|
||||
msprops_.active_style_type = current_style_type
|
||||
updated_materials.add(mat)
|
||||
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 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)]
|
||||
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
|
||||
|
||||
@@ -109,6 +109,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)
|
||||
@@ -160,6 +165,87 @@ 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
|
||||
|
||||
@staticmethod
|
||||
def _get_shader_label(material: bpy.types.Material, msprops) -> 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")
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
@@ -592,6 +592,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()
|
||||
@@ -669,9 +809,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:
|
||||
@@ -679,21 +989,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}"
|
||||
|
||||
@@ -739,3 +1069,30 @@ 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)
|
||||
|
||||
@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
|
||||
|
||||
Reference in New Issue
Block a user