From 04fe2bdf2eec4a22b52273a3e2d67ab53d52253b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 9 Jun 2023 18:39:50 +0500 Subject: [PATCH] External Styles Added support for external blender styles that will be saved to ifc. What it basically means that you can append material from external .blend file as a style that can be loaded in your blender material anytime. 1) External style can be added in Material Tab https://i.imgur.com/BYfesi5.png Note that you select material name at the right side of file selecting panel: https://i.imgur.com/mvqaaod.png 2) After adding external style you can switch between external and other surface styles: https://i.imgur.com/YtCz8an.png 3) There is also option to switch styles to External for all objects in the scene: https://i.imgur.com/u3zTcX9.png Some other changes: - some ui adjustments - moved functions that create blender shaders to tool.Loader (they also reset shader graph now so it's possible to use them on non-default blender materials) - some new tools for copying node graph from one material to another, appending data blocks - new operator for browsing external styles can be reused later to append other data blocks --- src/blenderbim/blenderbim/bim/import_ifc.py | 161 +------------- .../blenderbim/bim/module/misc/ui.py | 3 +- .../blenderbim/bim/module/style/__init__.py | 12 +- .../blenderbim/bim/module/style/data.py | 23 +- .../blenderbim/bim/module/style/operator.py | 200 +++++++++++++++++- .../blenderbim/bim/module/style/prop.py | 62 ++++++ .../blenderbim/bim/module/style/ui.py | 78 ++++++- src/blenderbim/blenderbim/core/style.py | 28 +++ src/blenderbim/blenderbim/tool/blender.py | 53 ++++- src/blenderbim/blenderbim/tool/loader.py | 174 +++++++++++++++ src/blenderbim/blenderbim/tool/style.py | 36 +++- 11 files changed, 651 insertions(+), 179 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index a7213eab5b..e85357c6b1 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -1490,173 +1490,18 @@ class IfcImporter: for surface_style in style.Styles: if surface_style.is_a() == "IfcSurfaceStyleShading": - self.create_surface_style_shading(blender_material, surface_style) + tool.Loader.create_surface_style_shading(blender_material, surface_style) elif surface_style.is_a("IfcSurfaceStyleRendering"): rendering_style = surface_style - self.create_surface_style_rendering(blender_material, surface_style) + 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: - self.create_surface_style_with_textures(blender_material, rendering_style, texture_style) + tool.Loader.create_surface_style_with_textures(blender_material, rendering_style, texture_style) tool.Style.record_shading(blender_material) - def create_surface_style_shading(self, blender_material, surface_style): - alpha = 1.0 - # Transparency was added in IFC4 - if hasattr(surface_style, "Transparency") and surface_style.Transparency: - alpha = 1 - surface_style.Transparency - blender_material.diffuse_color = ( - surface_style.SurfaceColour.Red, - surface_style.SurfaceColour.Green, - surface_style.SurfaceColour.Blue, - alpha, - ) - - def create_surface_style_rendering(self, blender_material, surface_style): - self.create_surface_style_shading(blender_material, surface_style) - if surface_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]: - blender_material.use_nodes = True - bsdf = blender_material.node_tree.nodes["Principled BSDF"] - if surface_style.DiffuseColour: - if surface_style.DiffuseColour.is_a("IfcColourRgb"): - bsdf.inputs["Base Color"].default_value = ( - surface_style.DiffuseColour.Red, - surface_style.DiffuseColour.Green, - surface_style.DiffuseColour.Blue, - 1, - ) - elif surface_style.DiffuseColour.is_a("IfcNormalisedRatioMeasure"): - bsdf.inputs["Base Color"].default_value = ( - surface_style.SurfaceColour.Red * surface_style.DiffuseColour.wrappedValue, - surface_style.SurfaceColour.Green * surface_style.DiffuseColour.wrappedValue, - surface_style.SurfaceColour.Blue * surface_style.DiffuseColour.wrappedValue, - 1, - ) - if surface_style.SpecularColour and surface_style.SpecularColour.is_a("IfcNormalisedRatioMeasure"): - bsdf.inputs["Metallic"].default_value = surface_style.SpecularColour.wrappedValue - if surface_style.SpecularHighlight and surface_style.SpecularHighlight.is_a("IfcSpecularRoughness"): - bsdf.inputs["Roughness"].default_value = surface_style.SpecularHighlight.wrappedValue - if hasattr(surface_style, "Transparency") and surface_style.Transparency: - bsdf.inputs["Alpha"].default_value = 1 - surface_style.Transparency - blender_material.blend_method = "BLEND" - elif surface_style.ReflectanceMethod == "FLAT": - blender_material.use_nodes = True - - output = {n.type: n for n in blender_material.node_tree.nodes}.get("OUTPUT_MATERIAL", None) - bsdf = blender_material.node_tree.nodes["Principled BSDF"] - - mix = blender_material.node_tree.nodes.new(type="ShaderNodeMixShader") - mix.location = bsdf.location - blender_material.node_tree.links.new(mix.outputs[0], output.inputs["Surface"]) - - blender_material.node_tree.nodes.remove(bsdf) - - lightpath = blender_material.node_tree.nodes.new(type="ShaderNodeLightPath") - lightpath.location = mix.location - mathutils.Vector((200, -200)) - blender_material.node_tree.links.new(lightpath.outputs[0], mix.inputs[0]) - - bsdf = blender_material.node_tree.nodes.new(type="ShaderNodeBsdfTransparent") - bsdf.location = mix.location - mathutils.Vector((200, 0)) - blender_material.node_tree.links.new(bsdf.outputs[0], mix.inputs[1]) - - rgb = blender_material.node_tree.nodes.new(type="ShaderNodeRGB") - rgb.location = mix.location - mathutils.Vector((200, 200)) - blender_material.node_tree.links.new(rgb.outputs[0], mix.inputs[2]) - - if surface_style.DiffuseColour and surface_style.DiffuseColour.is_a("IfcColourRgb"): - rgb.outputs[0].default_value = ( - surface_style.DiffuseColour.Red, - surface_style.DiffuseColour.Green, - surface_style.DiffuseColour.Blue, - 1, - ) - - def create_surface_style_with_textures(self, blender_material, rendering_style, texture_style): - for texture in texture_style.Textures: - mode = getattr(texture, "Mode", None) - node = None - - if texture.is_a("IfcImageTexture"): - image_url = texture.URLReference - if not os.path.abspath(texture.URLReference) and tool.Ifc.get_path(): - image_url = os.path.join(os.path.dirname(tool.Ifc.get_path()), texture.URLReference) - - if rendering_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]: - bsdf = blender_material.node_tree.nodes["Principled BSDF"] - if mode == "NORMAL": - normalmap = blender_material.node_tree.nodes.new(type="ShaderNodeNormalMap") - normalmap.location = bsdf.location - mathutils.Vector((200, 0)) - blender_material.node_tree.links.new(normalmap.outputs[0], bsdf.inputs["Normal"]) - - node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = normalmap.location - mathutils.Vector((200, 0)) - image = bpy.data.images.load(image_url) - image.colorspace_settings.name = "Non-Color" - node.image = image - blender_material.node_tree.links.new(node.outputs[0], normalmap.inputs["Color"]) - elif mode == "EMISSIVE": - output = {n.type: n for n in blender_material.node_tree.nodes}.get("OUTPUT_MATERIAL", None) - - add = blender_material.node_tree.nodes.new(type="ShaderNodeAddShader") - add.location = bsdf.location + mathutils.Vector((200, 0)) - blender_material.node_tree.links.new(bsdf.outputs[0], add.inputs[1]) - blender_material.node_tree.links.new(add.outputs[0], output.inputs[0]) - - emission = blender_material.node_tree.nodes.new(type="ShaderNodeEmission") - emission.location = add.location - mathutils.Vector((200, 0)) - blender_material.node_tree.links.new(emission.outputs[0], add.inputs[0]) - - node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = emission.location - mathutils.Vector((200, 0)) - image = bpy.data.images.load(image_url) - node.image = image - blender_material.node_tree.links.new(node.outputs[0], emission.inputs[0]) - elif mode == "METALLICROUGHNESS": - separate = blender_material.node_tree.nodes.new(type="ShaderNodeSeparateRGB") - separate.location = bsdf.location - mathutils.Vector((200, 0)) - blender_material.node_tree.links.new(separate.outputs[1], bsdf.inputs["Roughness"]) - blender_material.node_tree.links.new(separate.outputs[2], bsdf.inputs["Metallic"]) - - node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = separate.location - mathutils.Vector((200, 0)) - image = bpy.data.images.load(image_url) - image.colorspace_settings.name = "Non-Color" - node.image = image - blender_material.node_tree.links.new(node.outputs[0], separate.inputs[0]) - elif mode == "OCCLUSION": - # TODO work out how to implement glTF settings here - # https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html - pass - elif mode == "DIFFUSE": - node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = bsdf.location - mathutils.Vector((400, 0)) - image = bpy.data.images.load(image_url) - node.image = image - blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs["Base Color"]) - blender_material.node_tree.links.new(node.outputs[1], bsdf.inputs["Alpha"]) - blender_material.blend_method = "BLEND" - elif rendering_style.ReflectanceMethod == "FLAT": - bsdf = blender_material.node_tree.nodes["Mix Shader"] - if mode == "EMISSIVE": - node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = bsdf.location - mathutils.Vector((200, 0)) - image = bpy.data.images.load(image_url) - node.image = image - blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs[2]) - - if node and getattr(texture, "IsMappedBy", None): - coordinates = texture.IsMappedBy[0] - coord = blender_material.node_tree.nodes.new(type="ShaderNodeTexCoord") - coord.location = node.location - mathutils.Vector((200, 0)) - if coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD": - blender_material.node_tree.links.new(coord.outputs["Generated"], node.inputs["Vector"]) - elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE": - blender_material.node_tree.links.new(coord.outputs["Camera"], node.inputs["Vector"]) - else: - blender_material.node_tree.links.new(coord.outputs["UV"], node.inputs["Vector"]) - def place_objects_in_collections(self): for ifc_definition_id, obj in self.added_data.items(): if isinstance(obj, bpy.types.Object): diff --git a/src/blenderbim/blenderbim/bim/module/misc/ui.py b/src/blenderbim/blenderbim/bim/module/misc/ui.py index 278c8184e5..e706fc4caf 100644 --- a/src/blenderbim/blenderbim/bim/module/misc/ui.py +++ b/src/blenderbim/blenderbim/bim/module/misc/ui.py @@ -30,7 +30,8 @@ class BIM_PT_misc_utilities(bpy.types.Panel): def draw(self, context): layout = self.layout props = context.scene.BIMMiscProperties - + row = layout.row() + row.prop(context.scene.BIMStylesProperties, "active_style_type", icon="SHADING_RENDERED", text="") row = layout.split(factor=0.2, align=True) row.prop(props, "override_colour", text="") row.operator("bim.set_override_colour") diff --git a/src/blenderbim/blenderbim/bim/module/style/__init__.py b/src/blenderbim/blenderbim/bim/module/style/__init__.py index d8d6b7f344..c831f47d57 100644 --- a/src/blenderbim/blenderbim/bim/module/style/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/style/__init__.py @@ -21,10 +21,15 @@ from . import ui, prop, operator classes = ( operator.AddStyle, - operator.DisableEditingStyle, - operator.DisableEditingStyles, - operator.EditStyle, operator.EnableEditingStyle, + operator.DisableEditingStyle, + operator.EditStyle, + operator.EnableEditingExternalStyle, + operator.DisableEditingExternalStyle, + operator.EditExternalStyle, + operator.DisableEditingStyles, + operator.BrowseExternalStyle, + operator.ActivateExternalStyle, operator.LoadStyles, operator.RemoveStyle, operator.SelectByStyle, @@ -37,6 +42,7 @@ classes = ( ui.BIM_PT_styles, ui.BIM_PT_style, ui.BIM_PT_style_attributes, + ui.BIM_PT_external_style_attributes, ui.BIM_UL_styles, ) diff --git a/src/blenderbim/blenderbim/bim/module/style/data.py b/src/blenderbim/blenderbim/bim/module/style/data.py index 9826b994fa..f4fcbb56b5 100644 --- a/src/blenderbim/blenderbim/bim/module/style/data.py +++ b/src/blenderbim/blenderbim/bim/module/style/data.py @@ -56,7 +56,12 @@ class StyleAttributesData: @classmethod def load(cls): - cls.data = {"ifc_style_id": cls.ifc_style_id(), "attributes": cls.get_attributes()} + cls.data = { + "ifc_style_id": cls.ifc_style_id(), + "attributes": cls.get_attributes(), + "style_elements": cls.get_style_elements(), + } + cls.data["external_style_attributes"] = cls.get_external_style_attributes() cls.is_loaded = True @classmethod @@ -72,3 +77,19 @@ class StyleAttributesData: continue results.append({"name": name, "value": str(value)}) return results + + @classmethod + def get_style_elements(cls): + return tool.Style.get_style_elements(bpy.context.active_object.active_material) + + @classmethod + def get_external_style_attributes(cls): + external_style = cls.data["style_elements"].get("IfcExternallyDefinedSurfaceStyle", None) + if not external_style: + return None + results = [] + for name, value in external_style.get_info().items(): + if name in ["id", "type"]: + continue + results.append({"name": name, "value": str(value)}) + return results diff --git a/src/blenderbim/blenderbim/bim/module/style/operator.py b/src/blenderbim/blenderbim/bim/module/style/operator.py index 09e1215b1f..976016e6af 100644 --- a/src/blenderbim/blenderbim/bim/module/style/operator.py +++ b/src/blenderbim/blenderbim/bim/module/style/operator.py @@ -22,11 +22,15 @@ import blenderbim.tool as tool import blenderbim.core.style as core import ifcopenshell.util.representation from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.style.data import StylesData, StyleAttributesData +from pathlib import Path +import os class UpdateStyleColours(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.update_style_colours" - bl_label = "Update Style Colours" + bl_label = "Save Current Shading Style" + bl_description = "Save current style values to IfcSurfaceStyleShading" bl_options = {"REGISTER", "UNDO"} def _execute(self, context): @@ -103,6 +107,200 @@ class EditStyle(bpy.types.Operator, tool.Ifc.Operator): core.edit_style(tool.Ifc, tool.Style, obj=context.active_object.active_material) +class BrowseExternalStyle(bpy.types.Operator): + bl_idname = "bim.browse_external_style" + bl_label = "Browse External Style" + bl_options = {"REGISTER", "UNDO"} + + filepath: bpy.props.StringProperty( + name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" + ) + + filter_glob: bpy.props.StringProperty( + default="*.blend", + options={"HIDDEN"}, + ) + + def get_data_block_types(self, context): + return [("materials", "materials", "materials")] + # NOTE: the code below can be used later when we'll be adding other data-blocks besides materials + l = [("0", "", "")] + SUPPORTED_DATA_BLOCKS = ("materials", "textures", "brushes") + if os.path.exists(self.filepath) and self.filepath.endswith(".blend"): + with bpy.data.libraries.load(self.filepath) as (data_from, data_to): + for data_block_type in dir(data_from): + if data_block_type not in SUPPORTED_DATA_BLOCKS: + continue + data = getattr(data_from, data_block_type) + if data: + item = (data_block_type,) * 3 + l.append(item) + return l + + def get_data_blocks(self, context): + l = [("", "", "")] + if self.data_block_type != "0" and os.path.exists(self.filepath) and self.filepath.endswith(".blend"): + with bpy.data.libraries.load(self.filepath) as (data_from, data_to): + objects = getattr(data_from, self.data_block_type) + for o in objects: + l.append((o, o, o)) + return l + + data_block_type: bpy.props.EnumProperty( + name="Data Block Type", + description="List of data blocks in the .blend file", + items=get_data_block_types, + ) + + data_block: bpy.props.EnumProperty( + name="List of objects in the .blend file", + description="List of objects in the .blend file", + items=get_data_blocks, + ) + use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) + directory: bpy.props.StringProperty( + name="Directory", + description="Start file browsing directory", + default="", + ) + + def invoke(self, context, event): + mat = context.active_object.active_material + external_style = tool.Style.get_style_elements(mat).get("IfcExternallyDefinedSurfaceStyle", None) + # automatically select previously selected external style in file browser + if external_style and self.filepath == "": + style_path = Path(tool.Ifc.resolve_uri(external_style.Location)) + self.directory = str(style_path.parent) + self.filepath = str(style_path) + self.data_block_type, self.data_block = external_style.Identification.split("/") + + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + def draw(self, context): + layout = self.layout + layout.label(text="Data Block Type") + layout.prop(self, "data_block_type", text="", icon="GROUP") + layout.label(text="Data Block") + layout.prop(self, "data_block", text="") + if bpy.data.is_saved: + layout.prop(self, "use_relative_path") + else: + self.use_relative_path = False + layout.label(text="Save the .blend file first ") + layout.label(text="to use relative paths for .ifc.") + + def execute(self, context): + if self.data_block_type == "0": + self.report({"ERROR"}, "Select a data block type") + return {"CANCELLED"} + + if self.data_block == "": + self.report({"ERROR"}, "Select a data block") + return {"CANCELLED"} + + if not os.path.exists(self.filepath): + self.report({"ERROR"}, f"File not found:\n'{self.filepath}'") + return {"CANCELLED"} + + db = tool.Blender.append_data_block(self.filepath, self.data_block_type, self.data_block) + if not db["data_block"]: + self.report({"ERROR"}, db["msg"]) + return {"CANCELLED"} + + bpy.data.materials.remove(db["data_block"]) + + if not StyleAttributesData.is_loaded: + StyleAttributesData.load() + external_style = StyleAttributesData.data["style_elements"].get("IfcExternallyDefinedSurfaceStyle", None) + + if self.use_relative_path: + filepath = os.path.relpath(self.filepath, bpy.path.abspath("//")) + else: + filepath = self.filepath + + attributes = { + "Location": filepath, + "Identification": f"{self.data_block_type}/{self.data_block}", + "Name": self.data_block, + } + if not external_style: + core.add_external_style( + tool.Ifc, tool.Style, obj=context.active_object.active_material, attributes=attributes + ) + else: + core.update_external_style(tool.Ifc, tool.Style, external_style=external_style, attributes=attributes) + + StyleAttributesData.is_loaded = False + return {"FINISHED"} + + +class EnableEditingExternalStyle(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_external_style" + bl_label = "Enable Editing External Style" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.enable_editing_external_style(tool.Style, obj=context.active_object.active_material) + + +class DisableEditingExternalStyle(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.disable_editing_external_style" + bl_options = {"REGISTER", "UNDO"} + bl_label = "Disable Editing External Style" + + def _execute(self, context): + core.disable_editing_external_style(tool.Style, obj=context.active_object.active_material) + + +class EditExternalStyle(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_external_style" + bl_label = "Edit External Style" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.edit_external_style(tool.Ifc, tool.Style, obj=context.active_object.active_material) + + +class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.activate_external_style" + bl_label = "Activate External Style" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def _execute(self, context): + material = context.active_object.active_material + external_style = tool.Style.get_style_elements(material)["IfcExternallyDefinedSurfaceStyle"] + data_block_type, data_block = external_style.Identification.split("/") + style_path = Path(tool.Ifc.resolve_uri(external_style.Location)) + + if style_path.suffix != ".blend": + self.report({"ERROR"}, f"Only Blender external styles are supported") + return {"CANCELLED"} + + if not style_path.exists(): + self.report({"ERROR"}, f"File not found:\n'{style_path}'") + return {"CANCELLED"} + + db = tool.Blender.append_data_block(str(style_path), data_block_type, data_block) + if not db["data_block"]: + self.report({"ERROR"}, db["msg"]) + return {"CANCELLED"} + + props_to_copy = [ + "diffuse_color", + "metallic", + "roughness", + "specular_intensity", + "use_nodes", + ] + for prop_name in props_to_copy: + setattr(material, prop_name, getattr(db["data_block"], prop_name)) + + if material.use_nodes: + tool.Blender.copy_node_graph_to_active_object(context, db["data_block"]) + bpy.data.materials.remove(db["data_block"]) + + class DisableEditingStyles(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.disable_editing_styles" bl_options = {"REGISTER", "UNDO"} diff --git a/src/blenderbim/blenderbim/bim/module/style/prop.py b/src/blenderbim/blenderbim/bim/module/style/prop.py index f8666f8793..a44bb91919 100644 --- a/src/blenderbim/blenderbim/bim/module/style/prop.py +++ b/src/blenderbim/blenderbim/bim/module/style/prop.py @@ -31,6 +31,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +import blenderbim.tool as tool def get_style_types(self, context): @@ -45,13 +46,74 @@ class Style(PropertyGroup): total_elements: IntProperty(name="Total Elements") +STYLE_TYPES = [ + ("Shading", "Shading", ""), + ("External", "External", ""), +] + + +def update_shading_styles(self, context): + materials_to_objects = dict() + + for obj in bpy.data.objects: + blender_material = obj.active_material + if blender_material and blender_material.BIMMaterialProperties.ifc_style_id != 0: + materials_to_objects[blender_material] = obj + + for mat, obj in materials_to_objects.items(): + tool.Style.change_current_style_type(context, obj, mat, self.active_style_type) + + class BIMStylesProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") style_type: EnumProperty(items=get_style_types, name="Style Type") styles: CollectionProperty(name="Styles", type=Style) active_style_index: IntProperty(name="Active Style Index") + active_style_type: EnumProperty( + name="Active Style Type", + description="Update current blender material to match style type for all objects in the scene", + items=STYLE_TYPES, + default="Shading", + update=update_shading_styles, + ) + + +def update_shading_style(self, context): + blender_material = context.active_object.active_material + style_elements = tool.Style.get_style_elements(blender_material) + if self.active_style_type == "External": + if "IfcExternallyDefinedSurfaceStyle" in style_elements: + bpy.ops.bim.activate_external_style() + + elif self.active_style_type == "Shading": + style_elements = tool.Style.get_style_elements(blender_material) + rendering_style = None + texture_style = None + + for surface_style in style_elements.values(): + if surface_style.is_a() == "IfcSurfaceStyleShading": + tool.Loader.create_surface_style_shading(blender_material, surface_style) + elif surface_style.is_a("IfcSurfaceStyleRendering"): + rendering_style = surface_style + 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) class BIMStyleProperties(PropertyGroup): attributes: CollectionProperty(name="Attributes", type=Attribute) is_editing: BoolProperty(name="Is Editing") + + external_style_attributes: CollectionProperty(name="External Style Attributes", type=Attribute) + is_editing_external_style: BoolProperty(name="Is Editing External Style") + + active_style_type: EnumProperty( + name="Active Style Type", + description="Update current blender material to match style type", + items=STYLE_TYPES, + default="Shading", + update=update_shading_style, + ) diff --git a/src/blenderbim/blenderbim/bim/module/style/ui.py b/src/blenderbim/blenderbim/bim/module/style/ui.py index cefc9ff1f0..0d2fcdeba7 100644 --- a/src/blenderbim/blenderbim/bim/module/style/ui.py +++ b/src/blenderbim/blenderbim/bim/module/style/ui.py @@ -82,18 +82,22 @@ class BIM_PT_style(MaterialButtonsPanel, Panel): def draw(self, context): props = context.active_object.active_material.BIMMaterialProperties + style_props = context.active_object.active_material.BIMStyleProperties mat = context.material row = self.layout.row(align=True) - if props.ifc_style_id: - row.operator("bim.update_style_colours", icon="GREASEPENCIL") - row.operator("bim.update_style_textures", icon="TEXTURE", text="") - row.operator("bim.unlink_style", icon="UNLINKED", text="") - row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id - row = self.layout.row(align=True) - row.prop(mat, "diffuse_color", text="Color") - - else: + if not props.ifc_style_id: row.operator("bim.add_style", icon="ADD") + return + + row.prop(style_props, "active_style_type", icon="SHADING_RENDERED", text="") + row = self.layout.row(align=True) + row.operator("bim.update_style_colours", icon="GREASEPENCIL") + row.operator("bim.update_style_textures", icon="TEXTURE", text="") + row.operator("bim.unlink_style", icon="UNLINKED", text="") + row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id + + row = self.layout.row(align=True) + row.prop(mat, "diffuse_color", text="Viewport Color" if mat.use_nodes else "Render Color") class BIM_PT_style_attributes(Panel): @@ -102,6 +106,7 @@ class BIM_PT_style_attributes(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" + bl_parent_id = "BIM_PT_style" @classmethod def poll(cls, context): @@ -143,6 +148,61 @@ class BIM_PT_style_attributes(Panel): row.label(text=attribute["value"]) +class BIM_PT_external_style_attributes(Panel): + bl_label = "IFC External Surface Style" + bl_idname = "BIM_PT_external_style_attributes" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_parent_id = "BIM_PT_style" + + @classmethod + def poll(cls, context): + if not IfcStore.get_file(): + return False + try: + return bool(context.active_object.active_material.BIMMaterialProperties.ifc_style_id) + except: + return False + + def draw(self, context): + if not StyleAttributesData.is_loaded or ( + context.active_object.active_material.BIMMaterialProperties.ifc_style_id + != StyleAttributesData.data["ifc_style_id"] + ): + StyleAttributesData.load() + + mat = context.active_object.active_material + mprops = mat.BIMMaterialProperties + props = mat.BIMStyleProperties + + external_style = StyleAttributesData.data["style_elements"].get("IfcExternallyDefinedSurfaceStyle", None) + if not external_style: + row = self.layout.row(align=True) + row.operator("bim.browse_external_style", text="Add External Style", icon="ADD") + return + + row = self.layout.row(align=True) + row.label(text="Parameters:") + + if props.is_editing_external_style: + row.operator("bim.edit_external_style", icon="CHECKMARK", text="") + row.operator("bim.disable_editing_external_style", icon="CANCEL", text="") + blenderbim.bim.helper.draw_attributes(props.external_style_attributes, self.layout) + else: + row.operator("bim.browse_external_style", icon="APPEND_BLEND", text="") + row.operator("bim.enable_editing_external_style", icon="GREASEPENCIL", text="") + + row = self.layout.row(align=True) + row.label(text="STEP ID") + row.label(text=str(external_style.id())) + + for attribute in StyleAttributesData.data["external_style_attributes"]: + row = self.layout.row(align=True) + row.label(text=attribute["name"]) + row.label(text=attribute["value"]) + + class BIM_UL_styles(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: diff --git a/src/blenderbim/blenderbim/core/style.py b/src/blenderbim/blenderbim/core/style.py index c08ba48d09..e8eb462f9c 100644 --- a/src/blenderbim/blenderbim/core/style.py +++ b/src/blenderbim/blenderbim/core/style.py @@ -33,6 +33,17 @@ def add_style(ifc, style, obj=None): return element +def add_external_style(ifc, style, obj, attributes): + element = style.get_style(obj) + ifc.run( + "style.add_surface_style", style=element, ifc_class="IfcExternallyDefinedSurfaceStyle", attributes=attributes + ) + + +def update_external_style(ifc, style, external_style, attributes): + ifc.run("style.edit_surface_style", style=external_style, attributes=attributes) + + def remove_style(ifc, material, style_tool, style=None): obj = ifc.get_object(style) ifc.unlink(obj=obj, element=style) @@ -95,16 +106,33 @@ def enable_editing_style(style, obj=None): style.import_surface_attributes(style.get_style(obj), obj) +def enable_editing_external_style(style, obj=None): + external_style = style.get_style_elements(obj)["IfcExternallyDefinedSurfaceStyle"] + style.enable_editing_external_style(obj) + style.import_external_style_attributes(external_style, obj) + + def disable_editing_style(style, obj=None): style.disable_editing(obj) +def disable_editing_external_style(style, obj=None): + style.disable_editing_external_style(obj) + + def edit_style(ifc, style, obj=None): attributes = style.export_surface_attributes(obj) ifc.run("style.edit_presentation_style", style=style.get_style(obj), attributes=attributes) style.disable_editing(obj) +def edit_external_style(ifc, style, obj=None): + attributes = style.export_external_style_attributes(obj) + external_style = style.get_style_elements(obj)["IfcExternallyDefinedSurfaceStyle"] + update_external_style(ifc, style, external_style, attributes) + style.disable_editing_external_style(obj) + + def load_styles(style, style_type=None): style.import_presentation_styles(style_type) style.enable_editing_styles() diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 63ec2b6d01..44aca1ceab 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -108,11 +108,48 @@ class Blender: return context_override @classmethod - def update_viewport(cls): - # if it stops working in future Blender versions - # there is an alternative: - # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + def get_shader_editor_context(cls): + for screen in bpy.data.screens: + for area in screen.areas: + if area.type == "NODE_EDITOR": + for space in area.spaces: + if space.tree_type == "ShaderNodeTree": + context_override = {"area": area, "space": space, "screen": screen} + return context_override + @classmethod + def copy_node_graph_to_active_object(cls, context, material): + """make sure to override `context.active_object` + to you want change node graph for the object that's not actually active + """ + temp_override = cls.get_shader_editor_context() + old_material = context.active_object.active_material + new_material = material + + # remove all nodes from the current material + for n in old_material.node_tree.nodes[:]: + old_material.node_tree.nodes.remove(n) + + # change current material and make other window's shader editor is updated + context.active_object.active_material = new_material + temp_override["space"].node_tree = new_material.node_tree + + # select all nodes and copy them to clipboard + for node in new_material.node_tree.nodes: + node.select = True + bpy.ops.node.clipboard_copy(temp_override) + + # back to original material + context.active_object.active_material = old_material + temp_override["space"].node_tree = old_material.node_tree + bpy.ops.node.clipboard_paste(temp_override, offset=(0, 0)) + + @classmethod + def update_screen(cls): + bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1) + + @classmethod + def update_viewport(cls): tool.Blender.get_viewport_context()["area"].tag_redraw() @classmethod @@ -215,6 +252,14 @@ class Blender: context.view_layer.objects.active = active_object active_object.select_set(True) + @classmethod + def append_data_block(cls, filepath, data_block_type, name, link=False, relative=False): + with bpy.data.libraries.load(filepath, link=link, relative=relative) as (data_from, data_to): + if name not in getattr(data_from, data_block_type): + return {"data_block": None, "msg": f"Data-block {data_block_type}/{name} not found in {filepath}"} + getattr(data_to, data_block_type).append(name) + return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""} + ## BMESH UTILS ## @classmethod def apply_bmesh(cls, mesh, bm, obj=None): diff --git a/src/blenderbim/blenderbim/tool/loader.py b/src/blenderbim/blenderbim/tool/loader.py index 589751bbae..f0958b2e06 100644 --- a/src/blenderbim/blenderbim/tool/loader.py +++ b/src/blenderbim/blenderbim/tool/loader.py @@ -21,6 +21,8 @@ import bpy import ifcopenshell.util.element import blenderbim.core.tool import blenderbim.tool as tool +import os +import mathutils # Progressively we'll refactor loading elements into Blender objects into this @@ -28,6 +30,7 @@ import blenderbim.tool as tool # partially load and unload objects for huge models, partial model editing, and # supplementary objects (e.g. drawings, structural analysis models, etc). + class Loader(blenderbim.core.tool.Loader): @classmethod def get_mesh_name(cls, geometry): @@ -52,3 +55,174 @@ class Loader(blenderbim.core.tool.Loader): else: # TODO: See #2002 mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.replace(",", "")) + + @classmethod + def create_surface_style_shading(cls, blender_material, surface_style): + alpha = 1.0 + # Transparency was added in IFC4 + if hasattr(surface_style, "Transparency") and surface_style.Transparency: + alpha = 1 - surface_style.Transparency + blender_material.diffuse_color = ( + surface_style.SurfaceColour.Red, + surface_style.SurfaceColour.Green, + surface_style.SurfaceColour.Blue, + alpha, + ) + + @classmethod + def restart_material_node_tree(cls, blender_material): + nodes = blender_material.node_tree.nodes + links = blender_material.node_tree.links + for n in nodes[:]: + nodes.remove(n) + output = nodes.new("ShaderNodeOutputMaterial") + bsdf = nodes.new("ShaderNodeBsdfPrincipled") + links.new(bsdf.outputs["BSDF"], output.inputs["Surface"]) + + @classmethod + def create_surface_style_rendering(cls, blender_material, surface_style): + cls.create_surface_style_shading(blender_material, surface_style) + if surface_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]: + blender_material.use_nodes = True + cls.restart_material_node_tree(blender_material) + bsdf = blender_material.node_tree.nodes["Principled BSDF"] + if surface_style.DiffuseColour: + if surface_style.DiffuseColour.is_a("IfcColourRgb"): + bsdf.inputs["Base Color"].default_value = ( + surface_style.DiffuseColour.Red, + surface_style.DiffuseColour.Green, + surface_style.DiffuseColour.Blue, + 1, + ) + elif surface_style.DiffuseColour.is_a("IfcNormalisedRatioMeasure"): + bsdf.inputs["Base Color"].default_value = ( + surface_style.SurfaceColour.Red * surface_style.DiffuseColour.wrappedValue, + surface_style.SurfaceColour.Green * surface_style.DiffuseColour.wrappedValue, + surface_style.SurfaceColour.Blue * surface_style.DiffuseColour.wrappedValue, + 1, + ) + if surface_style.SpecularColour and surface_style.SpecularColour.is_a("IfcNormalisedRatioMeasure"): + bsdf.inputs["Metallic"].default_value = surface_style.SpecularColour.wrappedValue + if surface_style.SpecularHighlight and surface_style.SpecularHighlight.is_a("IfcSpecularRoughness"): + bsdf.inputs["Roughness"].default_value = surface_style.SpecularHighlight.wrappedValue + if hasattr(surface_style, "Transparency") and surface_style.Transparency: + bsdf.inputs["Alpha"].default_value = 1 - surface_style.Transparency + blender_material.blend_method = "BLEND" + + elif surface_style.ReflectanceMethod == "FLAT": + blender_material.use_nodes = True + cls.restart_material_node_tree(blender_material) + + output = {n.type: n for n in blender_material.node_tree.nodes}.get("OUTPUT_MATERIAL", None) + bsdf = blender_material.node_tree.nodes["Principled BSDF"] + + mix = blender_material.node_tree.nodes.new(type="ShaderNodeMixShader") + mix.location = bsdf.location + blender_material.node_tree.links.new(mix.outputs[0], output.inputs["Surface"]) + + blender_material.node_tree.nodes.remove(bsdf) + + lightpath = blender_material.node_tree.nodes.new(type="ShaderNodeLightPath") + lightpath.location = mix.location - mathutils.Vector((200, -200)) + blender_material.node_tree.links.new(lightpath.outputs[0], mix.inputs[0]) + + bsdf = blender_material.node_tree.nodes.new(type="ShaderNodeBsdfTransparent") + bsdf.location = mix.location - mathutils.Vector((200, 0)) + blender_material.node_tree.links.new(bsdf.outputs[0], mix.inputs[1]) + + rgb = blender_material.node_tree.nodes.new(type="ShaderNodeRGB") + rgb.location = mix.location - mathutils.Vector((200, 200)) + blender_material.node_tree.links.new(rgb.outputs[0], mix.inputs[2]) + + if surface_style.DiffuseColour and surface_style.DiffuseColour.is_a("IfcColourRgb"): + rgb.outputs[0].default_value = ( + surface_style.DiffuseColour.Red, + surface_style.DiffuseColour.Green, + surface_style.DiffuseColour.Blue, + 1, + ) + + @classmethod + def create_surface_style_with_textures(cls, blender_material, rendering_style, texture_style): + for texture in texture_style.Textures: + mode = getattr(texture, "Mode", None) + node = None + + if texture.is_a("IfcImageTexture"): + image_url = texture.URLReference + if not os.path.abspath(texture.URLReference) and tool.Ifc.get_path(): + image_url = os.path.join(os.path.dirname(tool.Ifc.get_path()), texture.URLReference) + + if rendering_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]: + bsdf = blender_material.node_tree.nodes["Principled BSDF"] + if mode == "NORMAL": + normalmap = blender_material.node_tree.nodes.new(type="ShaderNodeNormalMap") + normalmap.location = bsdf.location - mathutils.Vector((200, 0)) + blender_material.node_tree.links.new(normalmap.outputs[0], bsdf.inputs["Normal"]) + + node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") + node.location = normalmap.location - mathutils.Vector((200, 0)) + image = bpy.data.images.load(image_url) + image.colorspace_settings.name = "Non-Color" + node.image = image + blender_material.node_tree.links.new(node.outputs[0], normalmap.inputs["Color"]) + elif mode == "EMISSIVE": + output = {n.type: n for n in blender_material.node_tree.nodes}.get("OUTPUT_MATERIAL", None) + + add = blender_material.node_tree.nodes.new(type="ShaderNodeAddShader") + add.location = bsdf.location + mathutils.Vector((200, 0)) + blender_material.node_tree.links.new(bsdf.outputs[0], add.inputs[1]) + blender_material.node_tree.links.new(add.outputs[0], output.inputs[0]) + + emission = blender_material.node_tree.nodes.new(type="ShaderNodeEmission") + emission.location = add.location - mathutils.Vector((200, 0)) + blender_material.node_tree.links.new(emission.outputs[0], add.inputs[0]) + + node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") + node.location = emission.location - mathutils.Vector((200, 0)) + image = bpy.data.images.load(image_url) + node.image = image + blender_material.node_tree.links.new(node.outputs[0], emission.inputs[0]) + elif mode == "METALLICROUGHNESS": + separate = blender_material.node_tree.nodes.new(type="ShaderNodeSeparateRGB") + separate.location = bsdf.location - mathutils.Vector((200, 0)) + blender_material.node_tree.links.new(separate.outputs[1], bsdf.inputs["Roughness"]) + blender_material.node_tree.links.new(separate.outputs[2], bsdf.inputs["Metallic"]) + + node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") + node.location = separate.location - mathutils.Vector((200, 0)) + image = bpy.data.images.load(image_url) + image.colorspace_settings.name = "Non-Color" + node.image = image + blender_material.node_tree.links.new(node.outputs[0], separate.inputs[0]) + elif mode == "OCCLUSION": + # TODO work out how to implement glTF settings here + # https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html + pass + elif mode == "DIFFUSE": + node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") + node.location = bsdf.location - mathutils.Vector((400, 0)) + image = bpy.data.images.load(image_url) + node.image = image + blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs["Base Color"]) + blender_material.node_tree.links.new(node.outputs[1], bsdf.inputs["Alpha"]) + blender_material.blend_method = "BLEND" + elif rendering_style.ReflectanceMethod == "FLAT": + bsdf = blender_material.node_tree.nodes["Mix Shader"] + if mode == "EMISSIVE": + node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") + node.location = bsdf.location - mathutils.Vector((200, 0)) + image = bpy.data.images.load(image_url) + node.image = image + blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs[2]) + + if node and getattr(texture, "IsMappedBy", None): + coordinates = texture.IsMappedBy[0] + coord = blender_material.node_tree.nodes.new(type="ShaderNodeTexCoord") + coord.location = node.location - mathutils.Vector((200, 0)) + if coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD": + blender_material.node_tree.links.new(coord.outputs["Generated"], node.inputs["Vector"]) + elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE": + blender_material.node_tree.links.new(coord.outputs["Camera"], node.inputs["Vector"]) + else: + blender_material.node_tree.links.new(coord.outputs["UV"], node.inputs["Vector"]) diff --git a/src/blenderbim/blenderbim/tool/style.py b/src/blenderbim/blenderbim/tool/style.py index cfea6d1fce..cf2e58b0fa 100644 --- a/src/blenderbim/blenderbim/tool/style.py +++ b/src/blenderbim/blenderbim/tool/style.py @@ -33,6 +33,10 @@ class Style(blenderbim.core.tool.Style): def disable_editing(cls, obj): obj.BIMStyleProperties.is_editing = False + @classmethod + def disable_editing_external_style(cls, obj): + obj.BIMStyleProperties.is_editing_external_style = False + @classmethod def disable_editing_styles(cls): bpy.context.scene.BIMStylesProperties.is_editing = False @@ -41,6 +45,10 @@ class Style(blenderbim.core.tool.Style): def enable_editing(cls, obj): obj.BIMStyleProperties.is_editing = True + @classmethod + def enable_editing_external_style(cls, obj): + obj.BIMStyleProperties.is_editing_external_style = True + @classmethod def enable_editing_styles(cls): bpy.context.scene.BIMStylesProperties.is_editing = True @@ -49,6 +57,10 @@ class Style(blenderbim.core.tool.Style): def export_surface_attributes(cls, obj): return blenderbim.bim.helper.export_attributes(obj.BIMStyleProperties.attributes) + @classmethod + def export_external_style_attributes(cls, obj): + return blenderbim.bim.helper.export_attributes(obj.BIMStyleProperties.external_style_attributes) + @classmethod def get_active_style_type(cls): return bpy.context.scene.BIMStylesProperties.style_type @@ -73,6 +85,14 @@ class Style(blenderbim.core.tool.Style): except: return + @classmethod + def get_style_elements(cls, blender_material): + style = tool.Ifc.get().by_id(blender_material.BIMMaterialProperties.ifc_style_id) + style_elements = {} + for style in style.Styles: + style_elements[style.is_a()] = style + return style_elements + @classmethod def get_surface_rendering_attributes(cls, obj): transparency = 1 - obj.diffuse_color[3] @@ -197,8 +217,15 @@ class Style(blenderbim.core.tool.Style): @classmethod def import_surface_attributes(cls, style, obj): - obj.BIMStyleProperties.attributes.clear() - blenderbim.bim.helper.import_attributes2(style, obj.BIMStyleProperties.attributes) + attributes = obj.BIMStyleProperties.attributes + attributes.clear() + blenderbim.bim.helper.import_attributes2(style, attributes) + + @classmethod + def import_external_style_attributes(cls, style, obj): + attributes = obj.BIMStyleProperties.external_style_attributes + attributes.clear() + blenderbim.bim.helper.import_attributes2(style, attributes) @classmethod def is_editing_styles(cls): @@ -214,3 +241,8 @@ class Style(blenderbim.core.tool.Style): obj = tool.Ifc.get_object(element) if obj: obj.select_set(True) + + @classmethod + def change_current_style_type(cls, context, obj, blender_material, style_type): + with context.temp_override(active_object=obj): + blender_material.BIMStyleProperties.active_style_type = style_type