2
$\begingroup$

For those interested in the answer, I invite you to test the function below:

import bpy
    
def create_tex_image_nodes(mat):
    node_tree = mat.node_tree
    
    # Create nodes "TEX_IMAGE"
    image_nodes = []
    for i in range(5):
        node = node_tree.nodes.new(type='ShaderNodeTexImage')
        image_nodes.append(node)
    
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) # Work only if the Shader Editor is open
    
    for i, node in enumerate(image_nodes):
        node.update() # Not Work
        print(node.dimensions[:])
    
    return image_nodes
    
mat = bpy.context.object.active_material
tex_image_nodes = create_tex_image_nodes(mat)

I want to get the "Y" dimension of the nodes, but I can't update the dimensions, because as soon as the nodes are created, it always seems to be 0 so node.dimensions[:] is (0.0, 0.0)

Now I also tried using bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) but this only updates if the Shader Editor window is open on the affected material and node tree, in this case the dimensions[:] are (240.0, 202.0) and it is right.

Also node.update() does not update.

I also tried updating the node_tree or node_tree.nodes, but nothing works.

Any Idea about How to update the node.dimensions?

Please test this code, I've pasted it here just to make it easier.

Do 2 tests:

  • With shader editor in interface
  • Without Shader editor in interface

You immediately understand the difference.

Note

The developers themselves say it is expected behavior

"The dimensions of the node are found by drawing it, since they're influenced by the size of UI buttons"

$\endgroup$
10
  • $\begingroup$ try node.width and node.height $\endgroup$
    – Karan
    Commented Jun 5, 2023 at 18:25
  • $\begingroup$ node.dimensions is a read-only property, you can't edit it $\endgroup$
    – Karan
    Commented Jun 5, 2023 at 18:29
  • $\begingroup$ @Karan node.height is not the real size into the node_tree. node.dimensions is readonly, yes, My question is precisely how to get it, since it is not updated immediately when the nodes are created. n.height returns 100, this is not true in the node tree $\endgroup$
    – Noob Cat
    Commented Jun 5, 2023 at 19:15
  • $\begingroup$ Have you tried with an application timer ? docs.blender.org/api/current/bpy.app.timers.html waiting for 1 frame might give enough time for the value to initialize $\endgroup$
    – Gorgious
    Commented Jun 5, 2023 at 20:30
  • 1
    $\begingroup$ @NoobCat, node.update() is supposed to be used internally when the nodetree has changed ("adding and removing nodes and links"). You should not have to call it specifically. A node height is also an automatic value based on the node content and its dimensions can only be known when the node has been rendered at least one time (in the node editor). Without the rendering, the dimensions will allways be (0,0). But as nodes are by default expanded and its height is constant for it's sockets count, a TexImage will normally have an height of 290 units. $\endgroup$
    – Secrop
    Commented Jun 12, 2023 at 9:50

2 Answers 2

2
+50
$\begingroup$

Since node.dimensions get evaluated on UI redraw, we're limited to using bpy.ops.wm.redraw_timer(...) to trigger a redraw. The only problem that appears is that there has to be a ShaderEditor opened. The quickest and the easiest solution turned out to be switching the current area.ui_type to ShaderNodeTree and executing bpy.ops.wm.redraw_timer(...) just after. Below is the modified script.

Solution

Switch the current area.ui_type to ShaderNodeTree and we won't have to ensure Shader Editor is present.

import bpy


def remove_all_nodes(mat):
    nodes = mat.node_tree.nodes
    for node in reversed(nodes):
        nodes.remove(node)


def create_tex_image_nodes(context, mat):
    nodes = mat.node_tree.nodes

    # Create Image Texture nodes
    image_nodes = []
    for _ in range(5):
        node = nodes.new(type='ShaderNodeTexImage')
        image_nodes.append(node)

    # Change active area.ui_type to Shader Editor
    area = context.area
    old_area_ui_type = area.ui_type
    area.ui_type = 'ShaderNodeTree'

    # Redraw nodes in the node tree
    bpy.ops.wm.redraw_timer(type='DRAW_WIN', iterations=1)

    for node in image_nodes:
        print(node.dimensions[:])

    # Restore active area.ui_type
    area.ui_type = old_area_ui_type

    return image_nodes


context = bpy.context

mat = bpy.context.object.active_material
remove_all_nodes(mat)  # for testing
tex_image_nodes = create_tex_image_nodes(context, mat)

Console:

Warning: 1 x Draw window: 31.9231 ms, average: 31.92305565 ms
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)

I tested and seems like 'DRAW_WIN' is enough for bpy.ops.wm.redraw_timer(...). With 'DRAW_WIN_SWAP' there's a minor UI area flicker (on Linux at least).

The solution can be further improved to change area.ui_type only if there's no Shader Editor present on the screen, and with additional safety in case if active area is None.

Some may say that there could be a workaround with context override, but I couldn't get it working - blender would crash at the moment bpy.ops.wm.redraw_timer(...) was called.

Improved Solution

Change area.ui_type only if there's no Shader Editor present on the screen, and with additional safety in case if active area is None.

import bpy


def remove_all_nodes(mat):
    nodes = mat.node_tree.nodes
    for node in reversed(nodes):
        nodes.remove(node)


def get_prepared_ui_area(context):
    # Skip if Shader Editor exists
    for area in context.screen.areas:
        if area is None:
            continue

        if area.ui_type == 'ShaderNodeTree':
            return {'area': area, 'old_ui_type': area.ui_type}

    # Change area.ui_type to Shader Editor otherwise
    # Loop as a safety if the current area is None
    for area in context.screen.areas:
        if area is None:
            continue

        area_data = {'area': area, 'old_ui_type': area.ui_type}
        area.ui_type = 'ShaderNodeTree'
        return area_data

    # unlikely but present for code safety
    return {'area': None, 'old_ui_type': None}


def create_tex_image_nodes(context, mat):
    nodes = mat.node_tree.nodes

    # Create Image Texture nodes
    image_nodes = []
    for _ in range(5):
        node = nodes.new(type='ShaderNodeTexImage')
        image_nodes.append(node)

    area_data = get_prepared_ui_area(context)

    # Redraw nodes in the node tree
    bpy.ops.wm.redraw_timer(type='DRAW_WIN', iterations=1)

    for node in image_nodes:
        print(node.dimensions[:])

    # Restore active area.ui_type
    area = area_data['area']
    old_area_ui_type = area_data['old_ui_type']
    if area is not None and area.ui_type != old_area_ui_type:
        area.ui_type = old_area_ui_type

    return image_nodes


context = bpy.context

mat = bpy.context.object.active_material
remove_all_nodes(mat)  # for testing
tex_image_nodes = create_tex_image_nodes(context, mat)

Console (I ran the script four times):

Warning: 1 x Draw window: 31.9231 ms, average: 31.92305565 ms
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
Warning: 1 x Draw window: 19.2859 ms, average: 19.28591728 ms
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
Warning: 1 x Draw window: 19.6609 ms, average: 19.66094971 ms
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
Warning: 1 x Draw window: 24.5559 ms, average: 24.55592155 ms
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
(240.0, 202.0)
$\endgroup$
2
  • $\begingroup$ "Switch the current area.ui_type to ShaderNodeTree and we won't have to ensure _Shader Editor_ is present." - actually you don't have to assume/check if it is present, but you exactly ensure that it is. This switcheroo trick is nice, but it won't work if there's no interface to begin with… $\endgroup$ Commented Jun 20, 2023 at 9:11
  • $\begingroup$ yeah, good point. $\endgroup$
    – kemplerart
    Commented Jun 20, 2023 at 14:29
2
$\begingroup$

Get node.dimensions without open shader editor

This is ui scale 1.1, working without open shader editor

enter image description here

This is ui scale 1.6, working without open shader editor

enter image description here

Script with UI scale >= 1.0

import bpy
    
def create_tex_image_nodes(mat):
    # ui_scale = bpy.context.preferences.view.ui_scale
    node_tree = mat.node_tree
    node_tree.nodes.clear()
    
    # Create nodes "TEX_IMAGE"
    image_nodes = []
    for i in range(9):
        node = node_tree.nodes.new(type='ShaderNodeTexImage')
        image_nodes.append(node)
    
    # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) # Work only if the Shader Editor is open
    
    T = 0
    R = 0
    r = 0
    for i in range(3):
        for j in range(3):
            node = image_nodes[r]
            real_width = max(min(node.bl_width_default, node.bl_width_max), node.bl_width_min)
            # about 2 times the height
            fake_height = node.height * 2 # not work when ui_scale < 1
            node.location = R, T
            R += real_width
            r += 1
        T -= fake_height
        R = 0
    
    return image_nodes

mat = bpy.context.object.active_material
tex_image_nodes = create_tex_image_nodes(mat)

This is not possible using node.dimensions[1] to align the nodes when UI scale < 1.0 because node.dimensions is not always true

Here is the Reason

# suppose ui_scale = 0.75, Shader Editor is opened. tested on blender 3.5
# Align nodes using node.dimensions[1]
import bpy

# bpy.context.preferences.view.ui_scale = 0.75

def create_tex_image_nodes(mat):
    node_tree = mat.node_tree
    node_tree.nodes.clear()
    
    # Create nodes "TEX_IMAGE"
    image_nodes = []
    for i in range(9):
        node = node_tree.nodes.new(type='ShaderNodeTexImage')
        image_nodes.append(node)
    
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) # Work only if the Shader Editor is open
    
    T = 0
    R = 0
    r = 0
    for i in range(3):
        for j in range(3):
            node = image_nodes[r]
            real_width = max(min(node.bl_width_default, node.bl_width_max), node.bl_width_min)

            fake_height = node.dimensions[1]
            node.location = R, T
            R += real_width
            r += 1
        T -= fake_height
        R = 0
    
    return image_nodes

mat = bpy.context.object.active_material
tex_image_nodes = create_tex_image_nodes(mat)

Result:

enter image description here

$\endgroup$
4
  • $\begingroup$ Is the max(min( clamping necessary here? After all you create new nodes with their default sizes. If something else modifies the width, you're unable to read it, however. More importantly, if you decide to hide options (hiding sockets is more predictable ) you're unable to figure the height. BTW, I personally prefer max(MIN, min(value, MAX)) to put them in the MIN >= value >= MAX order. Also sorted((MIN, value, MAX))[1] is pretty cool. $\endgroup$ Commented Jun 20, 2023 at 9:30
  • $\begingroup$ the max(min( should not necessary and only work on first create. $\endgroup$
    – X Y
    Commented Jun 20, 2023 at 14:44
  • $\begingroup$ You're clamping the bl_width_default - can it ever be outside the clamped range? $\endgroup$ Commented Jun 20, 2023 at 14:46
  • $\begingroup$ The default value should be consistent with different ui scales, never saw the source code so I'm not sure. $\endgroup$
    – X Y
    Commented Jun 20, 2023 at 14:52

You must log in to answer this question.

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