3
$\begingroup$

I'm trying to copy nodes from one node tree/material to another, in python.

As far as I can tell there is no good way to do this.

I have even tried context switching and then either try to use

bpy.ops.node.clipboard_copy() or bpy.ops.node.group_make() on the selected nodes.

But both will fail due to wrong context, even though I have successfully changed the area.type to "NODE_EDITOR" or the area.ui_type to "ShaderNodeTree".

So, I'm at a loss here. I don't want to have to figure out the entire tree structure and re-create all individual nodes. I can select the nodes I need easily though. I would also be fine with somehow merging both trees and removing what I don't need.

Any help with this would be really appreciated. Blender 2.80. Just getting the node ops to work after a context switch to the node editor, would suffice already.

To clarify: I'm running my tool from VIEW_3D in object mode. I'm switching the context to the node editor like so:

def change_context(string):
    area = bpy.context.area
    old_type = area.type
    area.type = string
    return old_type

This works, in that it switches the 3d view to the node editor, but following this with calling bpy.ops.node.group_make() or bpy.ops.node.copy() will fail:

RuntimeError: Operator bpy.ops.node.group_make.poll() failed, context is incorrect

Calling the node ops from the search bar in proper, manually changed context works. So I suspect my context switching is incomplete?

edit2: video demo https://www.youtube.com/watch?v=N9tZLqHvcL4

  1. running group_make() from node editor, works fine
  2. switching area to node editor and running it, fails
$\endgroup$
10
  • $\begingroup$ Do you really need to switch the context? Where do you call it? $\endgroup$
    – brockmann
    Commented Nov 29, 2018 at 18:28
  • $\begingroup$ @jaroslav-jerryno-novotny I'm aware of these things, as I said, I don't want to recreate arbitarry node trees, node by node. $\endgroup$
    – MACHIN3
    Commented Nov 29, 2018 at 18:31
  • $\begingroup$ Ok, then I don't have a solution.. $\endgroup$ Commented Nov 29, 2018 at 18:32
  • 1
    $\begingroup$ Did you ever find a solution for this? I had the same approach before I found this and ran into the same issue. I stumbled upon this simple script which calls the poll function for all area types. I tried this with the 2.8 types for group_make and none of the types returned True. $\endgroup$ Commented Apr 22, 2020 at 10:40
  • 1
    $\begingroup$ With a little more testing I found the C code for the operators poll function and it seems like it is testing if the attribute space.edit_tree is not None. When you set the areas type to NODE_EDITOR it seems like that attribute is None while the operator is still running. Afterwards it's set though. I'll do some more testing to see if I can figure out how to set it. $\endgroup$ Commented Apr 22, 2020 at 16:50

3 Answers 3

4
$\begingroup$

After some experimenting, I couldn't find any way of doing this with bpy.data or with the operator for making a group, so I decided to write my own: This operator takes in the name of the node group that should be created and then moves all selected nodes from the actives object active material in a node group with that name. It also copies all the links between the nodes and the property values. It doesn't just copy the input values but also dropdowns and similar options.

Here's the code:

class NODE_OT_MakeGroup(bpy.types.Operator):
    bl_idname = "node.group_make_no_context"
    bl_label = "Make Context Free Group"
    bl_description = "Make group from selected nodes without context"
    bl_options = { "REGISTER", "UNDO" }

    name: bpy.props.StringProperty( default="NodeGroup" )

    @classmethod
    def poll( cls, context ):
        """run operator only if an active object with an active material exists"""

        if context.active_object:
            if context.active_object.active_material:
                if context.active_object.active_material.use_nodes:
                    return True
        return False


    def get_selected_nodes(self,context):
        """returns a list of the selected nodes from the active objects active material"""

        nodes = []
        for node in context.active_object.active_material.node_tree.nodes:
            if node.select:
                nodes.append(node)
        return nodes


    def copy_attributes(self, attributes, old_prop, new_prop):
        """copies the list of attributes from the old to the new prop if the attribute exists"""

        #check if the attribute exists and copy it
        for attr in attributes:
            if hasattr( new_prop, attr ):
                setattr( new_prop, attr, getattr( old_prop, attr ) )


    def get_node_attributes(self, node):
        """returns a list of all propertie identifiers if they shoulnd't be ignored"""

        #all attributes that shouldn't be copied
        ignore_attributes = ( "rna_type", "type", "dimensions", "inputs", "outputs", "internal_links", "select")

        attributes = []
        for attr in node.bl_rna.properties:
            #check if the attribute should be copied and add it to the list of attributes to copy
            if not attr.identifier in ignore_attributes and not attr.identifier.split("_")[0] == "bl":
                attributes.append(attr.identifier)

        return attributes


    def copy_nodes(self, nodes, group):
        """copies all nodes from the given list into the group with their attributes"""

        #the attributes that should be copied for every link
        input_attributes = ( "default_value", "name" )
        output_attributes = ( "default_value", "name" )

        for node in nodes:
            #create a new node in the group and find and copy its attributes
            new_node = group.nodes.new( node.bl_idname )
            node_attributes = self.get_node_attributes( node )
            self.copy_attributes( node_attributes, node, new_node )

            #copy the attributes for all inputs
            for i, inp in enumerate(node.inputs):
                self.copy_attributes( input_attributes, inp, new_node.inputs[i] )

            #copy the attributes for all outputs
            for i, out in enumerate(node.outputs):
                self.copy_attributes( output_attributes, out, new_node.outputs[i] )


    def copy_links(self, context, nodes, group):
        """copies all links between the nodes in the list to the nodes in the group"""

        for node in nodes:
            #find the corresponding node in the created group
            new_node = group.nodes[ node.name ]

            #enumerate over every link in the nodes inputs
            for i, inp in enumerate( node.inputs ):
                for link in inp.links:
                    #find the connected node for the link in the group
                    connected_node = group.nodes[ link.from_node.name ]
                    #connect the group nodes
                    group.links.new( connected_node.outputs[ link.from_socket.name ], new_node.inputs[i] )


    def add_group_nodes(self, group):
        """adds the group input and output node and positions them correctly"""

        #add group input and output
        group_input = group.nodes.new("NodeGroupInput")
        group_output = group.nodes.new("NodeGroupOutput")

        #if there are any nodes in the group, find the mini and maxi x position of all nodes and position the group nodes
        if len(group.nodes) > 0:
            min_pos = 9999999
            max_pos = -9999999

            for node in group.nodes:
                if node.location[0] < min_pos:
                    min_pos = node.location[0]
                elif node.location[0] + node.width > max_pos:
                    max_pos = node.location[0]

            group_input.location = (min_pos - 250, 0)
            group_output.location = (max_pos + 250, 0)


    def execute( self, context ):
        """creates the node group, copies all attributes and links and adds the group input and output"""

        #create new node group
        group = bpy.data.node_groups.new( name=self.name, type="ShaderNodeTree" )
        #get the list of all selected nodes from the active objects active material
        nodes = self.get_selected_nodes(context)

        #copy all nodes from the list to the created group with all their attributes
        self.copy_nodes( nodes, group )
        #copy the links between the nodes to the created groups nodes
        self.copy_links( context, nodes, group )

        #add the group input and output node to the created group
        self.add_group_nodes(group)

        return { "FINISHED" }

I'm sure there are some improvements that could be done to this and of course, it's not a perfect solution because that operator basically already exists, but I think it's the best option while these are not working. I tried it with 2.83 and it seems to work fine for all cases I tested.

EDIT: Here's a version that was better suited for my personal case. It has an additional parameter to specify a specific material. If it doesn't get a value for that parameter it defaults to the active objects active material. Otherwise, it's the same as the one above:

class NODE_OT_MakeGroup(bpy.types.Operator):
    bl_idname = "node.group_make_no_context"
    bl_label = "Make Context Free Group"
    bl_description = "Make group from selected nodes without context"
    bl_options = { "REGISTER", "UNDO" }

    name: bpy.props.StringProperty( default="NodeGroup" )
    material: bpy.props.StringProperty( default="")

    def get_selected_nodes(self,context):
        """returns a list of the selected nodes from the material"""

        nodes = []
        for node in bpy.data.materials[self.material].node_tree.nodes:
            if node.select:
                nodes.append(node)
        return nodes


    def copy_attributes(self, attributes, old_prop, new_prop):
        """copies the list of attributes from the old to the new prop if the attribute exists"""

        #check if the attribute exists and copy it
        for attr in attributes:
            if hasattr( new_prop, attr ):
                setattr( new_prop, attr, getattr( old_prop, attr ) )


    def get_node_attributes(self, node):
        """returns a list of all propertie identifiers if they shoulnd't be ignored"""

        #all attributes that shouldn't be copied
        ignore_attributes = ( "rna_type", "type", "dimensions", "inputs", "outputs", "internal_links", "select")

        attributes = []
        for attr in node.bl_rna.properties:
            #check if the attribute should be copied and add it to the list of attributes to copy
            if not attr.identifier in ignore_attributes and not attr.identifier.split("_")[0] == "bl":
                attributes.append(attr.identifier)

        return attributes


    def copy_nodes(self, nodes, group):
        """copies all nodes from the given list into the group with their attributes"""

        #the attributes that should be copied for every link
        input_attributes = ( "default_value", "name" )
        output_attributes = ( "default_value", "name" )

        for node in nodes:
            #create a new node in the group and find and copy its attributes
            new_node = group.nodes.new( node.bl_idname )
            node_attributes = self.get_node_attributes( node )
            self.copy_attributes( node_attributes, node, new_node )

            #copy the attributes for all inputs
            for i, inp in enumerate(node.inputs):
                self.copy_attributes( input_attributes, inp, new_node.inputs[i] )

            #copy the attributes for all outputs
            for i, out in enumerate(node.outputs):
                self.copy_attributes( output_attributes, out, new_node.outputs[i] )


    def copy_links(self, context, nodes, group):
        """copies all links between the nodes in the list to the nodes in the group"""

        for node in nodes:
            #find the corresponding node in the created group
            new_node = group.nodes[ node.name ]

            #enumerate over every link in the nodes inputs
            for i, inp in enumerate( node.inputs ):
                for link in inp.links:
                    #find the connected node for the link in the group
                    connected_node = group.nodes[ link.from_node.name ]
                    #connect the group nodes
                    group.links.new( connected_node.outputs[ link.from_socket.name ], new_node.inputs[i] )


    def add_group_nodes(self, group):
        """adds the group input and output node and positions them correctly"""

        #add group input and output
        group_input = group.nodes.new("NodeGroupInput")
        group_output = group.nodes.new("NodeGroupOutput")

        #if there are any nodes in the group, find the min and maxi x position of all nodes and position the group nodes
        if len(group.nodes) > 0:
            min_pos = 9999999
            max_pos = -9999999

            for node in group.nodes:
                if node.location[0] < min_pos:
                    min_pos = node.location[0]
                elif node.location[0] + node.width > max_pos:
                    max_pos = node.location[0]

            group_input.location = (min_pos - 250, 0)
            group_output.location = (max_pos + 250, 0)


    def execute( self, context ):
        """creates the node group, copies all attributes and links and adds the group input and output"""

        #set material if not given
        if not self.material:
            self.material = bpy.context.active_object.active_material.name

        #create new node group
        group = bpy.data.node_groups.new( name=self.name, type="ShaderNodeTree" )
        #get the list of all selected nodes from the active objects active material
        nodes = self.get_selected_nodes(context)

        #copy all nodes from the list to the created group with all their attributes
        self.copy_nodes( nodes, group )
        #copy the links between the nodes to the created groups nodes
        self.copy_links( context, nodes, group )

        #add the group input and output node to the created group
        self.add_group_nodes(group)

        return { "FINISHED" }
$\endgroup$
0
$\begingroup$

You can see here, the key is:bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1)

area.ui_type = 'ShaderNodeTree'
if area.spaces.active.node_tree != you_node_tree:
    area.spaces.active.shader_type ='OBJECT'
    bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1)
bpy.ops.node.clipboard_copy()

refer to: https://docs.blender.org/api/current/bpy.types.SpaceNodeEditor.html#bpy.types.SpaceNodeEditor

Update viewport while running script

$\endgroup$
0
$\begingroup$

Here's the example of copying nodes using clipboard:

import bpy

def get_shader_editor_context():
    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

def copy_node_graph_to_active_object(material):
    temp_override = get_shader_editor_context()
    old_material = bpy.context.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
    bpy.context.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
    bpy.context.object.active_material = old_material
    temp_override['space'].node_tree = old_material.node_tree
    bpy.ops.node.clipboard_paste(temp_override, offset=(0,0))


copy_node_graph_to_active_object(bpy.data.materials['test'])

```
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .