Module sverchok.utils.handle_blender_data

Expand source code
# This file is part of project Sverchok. It's copyrighted by the contributors
# recorded in the version control history of the file, available from
# its original location https://github.com/nortikin/sverchok/commit/master
#
# SPDX-License-Identifier: GPL3
# License-Filename: LICENSE

from __future__ import annotations

from collections.abc import Iterable
from enum import Enum
from functools import singledispatch, wraps, lru_cache, cached_property
from itertools import chain
from typing import Any, List, Union, TYPE_CHECKING, Optional

import bpy
from bpy.types import NodeInputs, NodeOutputs, NodeSocket

from sverchok.data_structure import fixed_iter

if TYPE_CHECKING:
    from sverchok.core.node_group import SvGroupTree
    from sverchok.node_tree import SverchCustomTree


# ~~~~ collection property functions ~~~~~


def correct_collection_length(collection: bpy.types.bpy_prop_collection, length: int) -> None:
    """
    It takes collection property and add or remove its items so it will be equal to given length
    If item has method `remove` it will be called before its deleting
    """
    if len(collection) < length:
        for i in range(len(collection), length):
            collection.add()
    elif len(collection) > length:
        for i in range(len(collection) - 1, length - 1, -1):
            try:
                collection[i].remove_data()
            except AttributeError:
                pass
            collection.remove(i)


# ~~~~ Blender collections functions ~~~~~


def pick_create_object(obj_name: str, data_block):
    """Find object with given name, if does not exist will create new object with given data bloc"""
    block = bpy.data.objects.get(obj_name)
    if not block:
        block = bpy.data.objects.new(name=obj_name, object_data=data_block)
    return block


def pick_create_data_block(collection: bpy.types.bpy_prop_collection, block_name: str):
    """
    Will find data block with given name in given collection (bpy.data.mesh, bpy.data.materials ,...)
    Don't use with objects collection
    If block does not exist new one will be created
     """
    block = collection.get(block_name)
    if not block:
        block = collection.new(name=block_name)
    return block


def delete_data_block(data_block) -> None:
    """
    It will delete such data like objects, meshes, materials
    It won't rise any error if give block does not exist in file anymore
    """
    @singledispatch
    def del_object(bl_obj) -> None:
        raise TypeError(f"Such type={type(bl_obj)} is not supported")

    @del_object.register
    def _(bl_obj: bpy.types.Object):
        bpy.data.objects.remove(bl_obj)

    @del_object.register
    def _(bl_obj: bpy.types.Mesh):
        bpy.data.meshes.remove(bl_obj)

    @del_object.register
    def _(bl_obj: bpy.types.Material):
        bpy.data.materials.remove(bl_obj)

    @del_object.register
    def _(bl_obj: bpy.types.Light):
        bpy.data.lights.remove(bl_obj)

    @del_object.register
    def _(bl_obj: bpy.types.Curve):
        bpy.data.curves.remove(bl_obj)

    try:
        del_object(data_block)
    except ReferenceError:
        # looks like already was deleted
        pass


def get_sv_trees():
    return [ng for ng in bpy.data.node_groups if ng.bl_idname in {'SverchCustomTreeType',}]


# ~~~~ encapsulation Blender objects ~~~~

# In general it's still arbitrary set of functionality (like module which fully consists with functions)
# But here the functions are combine with data which they handle


class BlDomains(Enum):
    # don't change the order - new items add to the end
    POINT = 'Point'
    EDGE = 'Edge'
    FACE = 'Face'
    CORNER = 'Face Corner'

    # It does not have sense to include these attributes because there is no API
    # for generating instances and applying attributes to a curve.
    # CURVE = 'Spline'
    # INSTANCE = 'Instance'


class BlObject:
    def __init__(self, obj):
        self._obj: bpy.types.Object = obj

    def set_attribute(self, values, attr_name, domain='POINT', value_type='FLOAT'):
        obj = self._obj
        attr = obj.data.attributes.get(attr_name)
        if attr is None:
            attr = obj.data.attributes.new(attr_name, value_type, domain)
        elif attr.data_type != value_type or attr.domain != domain:
            obj.data.attributes.remove(attr)
            attr = obj.data.attributes.new(attr_name, value_type, domain)

        if domain == 'POINT':
            amount = len(obj.data.vertices)
        elif domain == 'EDGE':
            amount = len(obj.data.edges)
        elif domain == 'CORNER':
            amount = len(obj.data.loops)
        elif domain == 'FACE':
            amount = len(obj.data.polygons)
        else:
            raise TypeError(f'Unsupported domain {domain}')

        if value_type in ['FLOAT', 'INT', 'BOOLEAN']:
            data = list(fixed_iter(values, amount))
        elif value_type in ['FLOAT_VECTOR', 'FLOAT_COLOR']:
            data = [co for v in fixed_iter(values, amount) for co in v]
        elif value_type == 'FLOAT2':
            data = [co for v in fixed_iter(values, amount) for co in v[:2]]
        else:
            raise TypeError(f'Unsupported type {value_type}')

        if value_type in ["FLOAT", "INT", "BOOLEAN"]:
            attr.data.foreach_set("value", data)
        elif value_type in ["FLOAT_VECTOR", "FLOAT2"]:
            attr.data.foreach_set("vector", data)
        else:
            attr.data.foreach_set("color", data)

        # attr.data.update()


class BlModifier:
    def __init__(self, modifier):
        self._mod: bpy.types.Modifier = modifier
        self.gn_tree: Optional[BlTree] = None  # cache for performance

    @property
    def node_group(self):
        return getattr(self._mod, 'node_group', None)

    @node_group.setter
    def node_group(self, node_group):
        self._mod.node_group = node_group

    def get_property(self, name):
        return getattr(self._mod, name)

    def set_property(self, name, value):
        setattr(self._mod, name, value)

    def get_tree_prop(self, name):
        return self._mod[name]

    def set_tree_prop(self, name, value):
        """Good for coping properties from one modifier to another"""
        self._mod[name] = value

    def set_tree_data(self, name, data, domain='POINT'):
        """Transfer py data to node modifier tree"""

        # transfer single value
        if not isinstance(data, (list, tuple)):
            data = [data]
        if not self.gn_tree.is_field(name) and len(data) != 1:
            data = data[:1]
        if len(data) == 1:
            value = data[0]
            self._mod[f"{name}_use_attribute"] = 0
            if isinstance(value, (list, tuple)):  # list of single vertex
                # for some reason node modifier can't apply python sequences directly
                for i, v in enumerate(value):
                    self._mod[name][i] = v
            else:
                sock = self.gn_tree.inputs[name]
                if sock.type in {'INT', 'BOOLEAN'}:
                    value = int(value)
                elif sock.type == 'VALUE':
                    value = float(value)
                elif sock.type == 'STRING':
                    value = str(value)
                self._mod[name] = value

        # transfer field
        else:
            self._mod[f"{name}_use_attribute"] = 1
            self._mod[f"{name}_attribute_name"] = name
            obj = BlObject(self._mod.id_data)
            sock = self.gn_tree.inputs[name]
            if sock.type in {'INT', 'BOOLEAN'} and not isinstance(data[0], int):
                data = [int(i) for i in data]
            bl_sock = BlSocket(sock)
            obj.set_attribute(data, name, domain, value_type=bl_sock.attribute_type)

    def remove(self):
        obj = self._mod.id_data
        obj.modifiers.remove(self._mod)
        self._mod = None

    @property
    def type(self) -> str:
        return self._mod.type

    def __eq__(self, other):
        if isinstance(other, BlModifier):
            # check type
            if self.type != other.type:
                return False

            # check properties
            for prop in (p for p in self._mod.bl_rna.properties if not p.is_readonly):
                if other.get_property(prop.identifier) != self.get_property(prop.identifier):
                    return False

            # check tree properties
            if self._mod.type == 'NODES' and self._mod.node_group:
                for tree_inp in self._mod.node_group.inputs[1:]:
                    prop_name = tree_inp.identifier
                    if self.get_tree_prop(prop_name) != other.get_tree_prop(prop_name):
                        return False
                    use_name = f"{prop_name}_use_attribute"
                    if self.get_tree_prop(use_name) != other.get_tree_prop(use_name):
                        return False
                    attr_name = f"{prop_name}_attribute_name"
                    if self.get_tree_prop(attr_name) != other.get_tree_prop(attr_name):
                        return False
                for tree_out in self._mod.node_group.outputs[1:]:
                    prop_name = f"{tree_out.identifier}_attribute_name"
                    if self.get_tree_prop(prop_name) != other.get_tree_prop(prop_name):
                        return False

            return True
        else:
            return NotImplemented


class BlTrees:
    """Wrapping around Blender tree, use with care
    it can crash if other containers are modified a lot
    https://docs.blender.org/api/current/info_gotcha.html#help-my-script-crashes-blender
    All this is True and about Blender class itself"""

    MAIN_TREE_ID = 'SverchCustomTreeType'
    GROUP_ID = 'SvGroupTree'

    def __init__(self, node_groups=None):
        self._trees = node_groups

    @classmethod
    def is_main_tree(cls, tree):
        return tree.bl_idname == cls.MAIN_TREE_ID

    @property
    def sv_trees(self) -> Iterable[Union[SverchCustomTree, SvGroupTree]]:
        """All Sverchok trees in a file or in given set of trees"""
        trees = self._trees or bpy.data.node_groups
        return (t for t in trees if t.bl_idname in [self.MAIN_TREE_ID, self.GROUP_ID])

    @property
    def sv_main_trees(self) -> Iterable[SverchCustomTree]:
        """All main Sverchok trees in a file or in given set of trees"""
        trees = self._trees or bpy.data.node_groups
        return (t for t in trees if t.bl_idname == self.MAIN_TREE_ID)

    @property
    def sv_group_trees(self) -> Iterable[SvGroupTree]:
        """All Sverchok group trees"""
        trees = self._trees or bpy.data.node_groups
        return (t for t in trees if t.bl_idname == self.GROUP_ID)


class BlTree:
    def __init__(self, tree):
        self._tree = tree
        self.inputs = {s.identifier: s for s in tree.inputs}
        self.outputs = {s.identifier: s for s in tree.outputs}

        self.is_field = lru_cache(self._is_field)  # for performance

    @cached_property
    def group_input(self):
        for node in self._tree.nodes:
            if node.bl_idname == 'NodeGroupInput':
                return node
        return None

    def _is_field(self, input_socket_identifier):
        """Check whether input tree socket expects field (diamond socket)"""
        if (group := self.group_input) is None:
            raise LookupError(f'Group input node is required '
                              f'which is not found in "{self._tree.name}" tree')
        sock = BlSocket.from_identifier(group.outputs, input_socket_identifier)
        return sock.display_shape == 'DIAMOND'


class BlNode:
    """Wrapping around ordinary node for extracting some its information"""
    DEBUG_NODES_IDS = {'SvDebugPrintNode', 'SvStethoscopeNode'}  # can be added as Mix-in class

    def __init__(self, node):
        self.data = node

    @property
    def properties(self) -> List[BPYProperty]:
        """Iterator over all node properties"""
        node_properties = self.data.bl_rna.__annotations__ if hasattr(self.data.bl_rna, '__annotations__') else []
        return [BPYProperty(self.data, prop_name) for prop_name in node_properties]

    @property
    def is_debug_node(self) -> bool:
        """Nodes which print sockets content"""
        return self.base_idname in self.DEBUG_NODES_IDS

    @property
    def base_idname(self) -> str:
        """SvStethoscopeNodeMK2 -> SvStethoscopeNode
        it won't parse more tricky variants like SvStethoscopeMK2Node which I saw exists"""
        id_name, _, version = self.data.bl_idname.partition('MK')
        try:
            int(version)
        except ValueError:
            return self.data.bl_idname
        return id_name


class BlSockets:
    def __init__(self, sockets: Union[NodeInputs, NodeOutputs]):
        self._sockets = sockets

    def copy_sockets(self, sockets_from: Iterable):
        """Copy sockets from one collection to another. Also, it can be used
        to refresh `to` collection to be equal to `from` collection and in this
        case only new socket will be added and old one removed.
        It can copy properties:
        sv socket -> sv socket
        sv interface socket -> sv socket
        """
        sockets_to = self._sockets
        # remove sockets which are not presented in from collection
        identifiers_from = {s.identifier for s in sockets_from}
        for s_to in sockets_to:
            if s_to.identifier not in identifiers_from:
                sockets_to.remove(s_to)

        # add new sockets
        sock_indexes_to = {s.identifier: i for i, s in enumerate(sockets_to)}
        for s_from in sockets_from:
            if s_from.identifier in sock_indexes_to:
                continue
            id_name = getattr(s_from, 'bl_socket_idname', s_from.bl_idname)
            s_to = sockets_to.new(id_name, s_from.name, identifier=s_from.identifier)
            sock_indexes_to[s_to.identifier] = len(sockets_to) - 1

        # fix existing sockets
        for s_from in sockets_from:
            s_to = sockets_to[sock_indexes_to[s_from.identifier]]
            id_name = getattr(s_from, 'bl_socket_idname', s_from.bl_idname)
            if id_name != s_to.bl_idname:
                s_to = s_to.replace_socket(id_name)

        # fix socket positions
        for new_pos, s_from in enumerate(sockets_from):
            current_pos = sock_indexes_to[s_from.identifier]
            if current_pos != new_pos:
                sockets_to.move(current_pos, new_pos)
                sock_indexes_to = {
                    s.identifier: i for i, s in enumerate(sockets_to)}


class BlSocket:
    _attr_types = {
        'VECTOR': 'FLOAT_VECTOR',
        'VALUE': 'FLOAT',
        'RGBA': 'FLOAT_COLOR',
        'INT': 'INT',
        'BOOLEAN': 'BOOLEAN',
    }

    _sv_types = {
        'VECTOR': 'SvVerticesSocket',
        'VALUE': 'SvStringsSocket',
        'RGBA': 'SvColorSocket',
        'INT': 'SvStringsSocket',
        'STRING': 'SvTextSocket',
        'BOOLEAN': 'SvStringsSocket',
        'OBJECT': 'SvObjectSocket',
        'COLLECTION': 'SvCollectionSocket',
        'MATERIAL': 'SvMaterialSocket',
        'TEXTURE': 'SvTextureSocket',
        'IMAGE': 'SvImageSocket',
    }

    def __init__(self, socket):
        self._sock: bpy.types.NodeSocket = socket

    def copy_properties(self, sv_sock):
        sv_sock.name = self._sock.name

        if sv_sock.bl_idname == 'SvStringsSocket':
            if self._sock.type == 'VALUE':
                sv_sock.default_property_type = 'float'
            elif self._sock.type in {'INT', 'BOOLEAN'}:
                sv_sock.default_property_type = 'int'
            else:
                return  # There is no default property for such type
            if sv_sock.default_property == 0:  # was unchanged by user
                sv_sock.default_property = self._sock.default_value
                sv_sock.use_prop = True

        elif sv_sock.bl_idname == 'SvVerticesSocket':
            if sv_sock.default_property[:] == (0, 0, 0):  # was unchanged by user
                sv_sock.default_property = self._sock.default_value
                sv_sock.use_prop = True

        elif sv_sock.bl_idname == 'SvObjectSocket':
            if sv_sock.default_property is None:  # was unchanged by user
                sv_sock.object_ref_pointer = self._sock.default_value
                sv_sock.use_prop = True

        elif hasattr(sv_sock, 'default_property'):
            sv_default = BPYProperty(sv_sock, 'default_property').default_value
            if isinstance(sv_sock.default_property, bpy.types.bpy_prop_array):
                current = sv_sock.default_property[:]
            else:
                current = sv_sock.default_property
            if sv_default != current:
                return  # the value was already changed by user
            sv_sock.default_property = self._sock.default_value
            sv_sock.use_prop = True

    @classmethod
    def from_identifier(cls, sockets, identifier):
        for s in sockets:
            if s.identifier == identifier:
                return cls(s)
        raise LookupError(f"Socket with {identifier=} was not found")

    @property
    def attribute_type(self):
        return self._attr_types[self._sock.type]

    @property
    def sverchok_type(self):
        if (sv_type := self._sv_types.get(self._sock.type)) is None:
            return 'SvStringsSocket'
        return sv_type

    @property
    def display_shape(self):
        return self._sock.display_shape


class BPYProperty:
    """Wrapper over any property to get access to advance information"""
    def __init__(self, data, prop_name: str):
        """
        Data block can be any blender data like, node, trees, sockets
        Use with caution as far it keeps straight reference to Blender object
        """
        self.name = prop_name
        self._data = data

    @property
    def is_valid(self) -> bool:
        """
        If data does not have property with given name property is invalid
        It can be so that data.keys() or data.items() can give names of properties which are not in data class any more
        Such properties cab consider as deprecated
        """
        return self.name in self._data.bl_rna.properties

    @property
    def value(self) -> Any:
        """Returns value of the property in Python format, list of dicts for collection, tuple for array"""
        if not self.is_valid:
            raise TypeError(f'Can not read "value" of invalid property "{self.name}"')
        elif self.is_array_like:
            return tuple(getattr(self._data, self.name))
        elif self.type == 'COLLECTION':
            return self._extract_collection_values()
        elif self.type == 'POINTER':  # it simply returns name of data block or None
            data_block = getattr(self._data, self.name)
            return data_block.name if data_block is not None else None
        else:
            return getattr(self._data, self.name)

    @value.setter
    def value(self, value):
        """Apply values in python format to the property"""
        if not self.is_valid:
            raise TypeError(f'Can not read "value" of invalid property "{self.name}"')
        if self.type == 'COLLECTION':
            self._set_collection_values(value)
        elif self.type == 'POINTER' and isinstance(value, str):
            setattr(self._data, self.name, self.data_collection.get(value))
        elif self.type == 'ENUM' and self.is_array_like:
            setattr(self._data, self.name, set(value))
        else:
            setattr(self._data, self.name, value)

    @property
    def type(self) -> str:
        """Type of property: STRING, FLOAT, INT, POINTER, COLLECTION"""
        if not self.is_valid:
            raise TypeError(f'Can not read "type" of invalid property "{self.name}"')
        return self._data.bl_rna.properties[self.name].type

    @property
    def pointer_type(self) -> BPYPointers:
        if self.type != 'POINTER':
            raise TypeError(f'This property is only valid for "POINTER" types, {self.type} type is given')
        return BPYPointers.get_type(self._data.bl_rna)

    @property
    def default_value(self) -> Any:
        """Returns default value, None for pointers, list of dicts of default values for collections"""
        if not self.is_valid:
            raise TypeError(f'Can not read "default_value" of invalid property "{self.name}"')
        elif self.type == 'COLLECTION':
            return self._extract_collection_values(default_value=True)
        elif self.is_array_like:
            if self.type == 'ENUM':
                return tuple(self._data.bl_rna.properties[self.name].default_flag)
            else:
                return tuple(self._data.bl_rna.properties[self.name].default_array)
        elif self.type == 'POINTER':
            return None
        else:
            return self._data.bl_rna.properties[self.name].default

    @property
    def pointer_type(self) -> BPYPointers:
        """It returns subtypes of POINTER type"""
        if self.type != 'POINTER':
            raise TypeError(f'Only POINTER property type has `pointer_type` attribute, "{self.type}" given')
        return BPYPointers.get_type(self._data.bl_rna.properties[self.name].fixed_type)

    @property
    def data_collection(self):
        """For pointer properties only, if pointer type is MESH it will return bpy.data.meshes"""
        return self.pointer_type.collection

    @property
    def is_to_save(self) -> bool:
        """False if property has option BoolProperty(options={'SKIP_SAVE'})"""
        if not self.is_valid:
            raise TypeError(f'Can not read "is_to_save" of invalid property "{self.name}"')
        return not self._data.bl_rna.properties[self.name].is_skip_save

    @property
    def is_array_like(self) -> bool:
        """True for VectorArray, FloatArray, IntArray, Enum with enum flag"""
        if not self.is_valid:
            raise TypeError(f'Can not read "is_array_like" of invalid property "{self.name}"')
        if self.type in {'BOOLEAN', 'FLOAT', 'INT'}:
            return self._data.bl_rna.properties[self.name].is_array
        elif self.type == 'ENUM':
            # Enum can return set of values, array like
            return self._data.bl_rna.properties[self.name].is_enum_flag
        else:
            # other properties does not have is_array attribute
            return False

    def unset(self):
        """Assign default value to the property"""
        self._data.property_unset(self.name)

    def filter_collection_values(self, skip_default=True, skip_save=True):
        """Convert data of collection property into python format with skipping certain properties"""
        if self.type != 'COLLECTION':
            raise TypeError(f'Method supported only "collection" types, "{self.type}" was given')
        if not self.is_valid:
            raise TypeError(f'Can not read "non default collection values" of invalid property "{self.name}"')
        items = []
        for item in getattr(self._data, self.name):
            item_props = {}

            # in some nodes collections are getting just PropertyGroup type instead of its subclasses
            # PropertyGroup itself does not have any properties
            item_properties = item.__annotations__ if hasattr(item, '__annotations__') else []
            for prop_name in chain(['name'], item_properties):  # item.items() will return only changed values
                prop = BPYProperty(item, prop_name)
                if not prop.is_valid:
                    continue
                if skip_save and not prop.is_to_save:
                    continue
                if prop.type != 'COLLECTION':
                    if skip_default and prop.default_value == prop.value:
                        continue
                    item_props[prop.name] = prop.value
                else:
                    item_props[prop.name] = prop.filter_collection_values(skip_default, skip_save)
            items.append(item_props)
        return items

    def collection_to_list(self):
        """Returns data structure like this [[p1, p2, p3], [p4, p5, p6]]
        in this example the collection has two items, each item has 3 properties"""
        if self.type != 'COLLECTION':
            raise TypeError(f'Method supported only "collection" types, "{self.type}" was given')
        if not self.is_valid:
            raise TypeError(f'Can not read "non default collection values" of invalid property "{self.name}"')

        collection = []
        for item in getattr(self._data, self.name):
            prop_list = []
            # in some nodes collections are getting just PropertyGroup type instead of its subclasses
            # PropertyGroup itself does not have any properties
            item_properties = item.__annotations__ if hasattr(item, '__annotations__') else []
            for prop_name in chain(['name'], item_properties):  # item.items() will return only changed values
                prop = BPYProperty(item, prop_name)
                prop_list.append(prop)
            collection.append(prop_list)
        return collection

    def _extract_collection_values(self, default_value: bool = False):
        """returns something like this: [{"name": "", "my_prop": 1.0}, {"name": "", "my_prop": 2.0}, ...]"""
        items = []
        for item in getattr(self._data, self.name):
            item_props = {}

            # in some nodes collections are getting just PropertyGroup type instead of its subclasses
            # PropertyGroup itself does not have any properties
            item_properties = item.__annotations__ if hasattr(item, '__annotations__') else []
            for prop_name in chain(['name'], item_properties):  # item.items() will return only changed values
                prop = BPYProperty(item, prop_name)
                if prop.is_valid:
                    item_props[prop.name] = prop.default_value if default_value else prop.value
            items.append(item_props)
        return items

    def _set_collection_values(self, value: List[dict]):
        """Assign Python data to collection property"""
        collection = getattr(self._data, self.name)
        for item_index, item_values in enumerate(value):
            # Some collections can be empty, in this case they should be expanded to be able to get new values
            if item_index == len(collection):
                item = collection.add()
            else:
                item = collection[item_index]
            for prop_name, prop_value in item_values.items():
                prop = BPYProperty(item, prop_name)
                if prop.is_valid:
                    prop.value = prop_value


class BPYPointers(Enum):
    """
    Pointer types which are used in Sverchok
    New properties should be added with updating collection property
    """
    # pointer name = type of data
    OBJECT = bpy.types.Object
    MESH = bpy.types.Mesh
    NODE_TREE = bpy.types.NodeTree
    NODE = bpy.types.Node  # there is pointers to nodes in Blender like node.parent property
    MATERIAL = bpy.types.Material
    COLLECTION = bpy.types.Collection
    TEXT = bpy.types.Text
    LIGHT = bpy.types.Light
    IMAGE = bpy.types.Image
    TEXTURE = bpy.types.Texture
    VECTOR_FONT = bpy.types.VectorFont
    GREASE_PENCIL = bpy.types.GreasePencil

    @property
    def collection(self):
        """Map of pointer type and its collection"""
        collections = {
            BPYPointers.OBJECT: bpy.data.objects,
            BPYPointers.MESH: bpy.data.meshes,
            BPYPointers.NODE_TREE: bpy.data.node_groups,
            BPYPointers.NODE: None,
            BPYPointers.MATERIAL:  bpy.data.materials,
            BPYPointers.COLLECTION: bpy.data.collections,
            BPYPointers.TEXT: bpy.data.texts,
            BPYPointers.LIGHT: bpy.data.lights,
            BPYPointers.IMAGE: bpy.data.images,
            BPYPointers.TEXTURE: bpy.data.textures,
            BPYPointers.VECTOR_FONT: bpy.data.curves,
            BPYPointers.GREASE_PENCIL: bpy.data.grease_pencils
        }
        return collections[self]

    @property
    def type(self):
        """Return Blender type of the pointer"""
        return self.value

    @classmethod
    def get_type(cls, bl_rna) -> Union[BPYPointers, None]:
        """Return Python pointer corresponding to given Blender pointer class (bpy.types.Mesh.bl_rna)"""
        for pointer in BPYPointers:
            if pointer.type.bl_rna == bl_rna or pointer.type.bl_rna == bl_rna.base:
                return pointer
        raise TypeError(f'Type: "{bl_rna}" was not found in: {[t.type.bl_rna for t in BPYPointers]}')


def get_func_and_args(prop):
    """
    usage:   
        - formerly
        prop_func, prop_args = some_class.__annotations__[some_prop_name]
        - new
        prop_to_decompose = some_class.__annotations__[some_prop_name]
        prop_func, prop_args = get_func_and_args(prop_to_decompose)
        
    """
    if hasattr(prop, "keywords"):
        return prop.function, prop.keywords
    else:
        return prop


def keep_enum_reference(enum_func):
    """remember you have to keep enum strings somewhere in py,
    else they get freed and Blender references invalid memory!
    This decorator should be used for these purposes"""
    saved_items = dict()

    @wraps(enum_func)
    def wrapper(node, context):
        nonlocal saved_items
        items = enum_func(node, context)
        saved_items[node.node_id] = items
        return saved_items[node.node_id]
    wrapper.keep_ref = True  # just for tests
    return wrapper

Functions

def correct_collection_length(collection: bpy.types.bpy_prop_collection, length: int) ‑> None

It takes collection property and add or remove its items so it will be equal to given length If item has method remove it will be called before its deleting

Expand source code
def correct_collection_length(collection: bpy.types.bpy_prop_collection, length: int) -> None:
    """
    It takes collection property and add or remove its items so it will be equal to given length
    If item has method `remove` it will be called before its deleting
    """
    if len(collection) < length:
        for i in range(len(collection), length):
            collection.add()
    elif len(collection) > length:
        for i in range(len(collection) - 1, length - 1, -1):
            try:
                collection[i].remove_data()
            except AttributeError:
                pass
            collection.remove(i)
def delete_data_block(data_block) ‑> None

It will delete such data like objects, meshes, materials It won't rise any error if give block does not exist in file anymore

Expand source code
def delete_data_block(data_block) -> None:
    """
    It will delete such data like objects, meshes, materials
    It won't rise any error if give block does not exist in file anymore
    """
    @singledispatch
    def del_object(bl_obj) -> None:
        raise TypeError(f"Such type={type(bl_obj)} is not supported")

    @del_object.register
    def _(bl_obj: bpy.types.Object):
        bpy.data.objects.remove(bl_obj)

    @del_object.register
    def _(bl_obj: bpy.types.Mesh):
        bpy.data.meshes.remove(bl_obj)

    @del_object.register
    def _(bl_obj: bpy.types.Material):
        bpy.data.materials.remove(bl_obj)

    @del_object.register
    def _(bl_obj: bpy.types.Light):
        bpy.data.lights.remove(bl_obj)

    @del_object.register
    def _(bl_obj: bpy.types.Curve):
        bpy.data.curves.remove(bl_obj)

    try:
        del_object(data_block)
    except ReferenceError:
        # looks like already was deleted
        pass
def get_func_and_args(prop)

usage:
- formerly prop_func, prop_args = some_class.annotations[some_prop_name] - new prop_to_decompose = some_class.annotations[some_prop_name] prop_func, prop_args = get_func_and_args(prop_to_decompose)

Expand source code
def get_func_and_args(prop):
    """
    usage:   
        - formerly
        prop_func, prop_args = some_class.__annotations__[some_prop_name]
        - new
        prop_to_decompose = some_class.__annotations__[some_prop_name]
        prop_func, prop_args = get_func_and_args(prop_to_decompose)
        
    """
    if hasattr(prop, "keywords"):
        return prop.function, prop.keywords
    else:
        return prop
def get_sv_trees()
Expand source code
def get_sv_trees():
    return [ng for ng in bpy.data.node_groups if ng.bl_idname in {'SverchCustomTreeType',}]
def keep_enum_reference(enum_func)

remember you have to keep enum strings somewhere in py, else they get freed and Blender references invalid memory! This decorator should be used for these purposes

Expand source code
def keep_enum_reference(enum_func):
    """remember you have to keep enum strings somewhere in py,
    else they get freed and Blender references invalid memory!
    This decorator should be used for these purposes"""
    saved_items = dict()

    @wraps(enum_func)
    def wrapper(node, context):
        nonlocal saved_items
        items = enum_func(node, context)
        saved_items[node.node_id] = items
        return saved_items[node.node_id]
    wrapper.keep_ref = True  # just for tests
    return wrapper
def pick_create_data_block(collection: bpy.types.bpy_prop_collection, block_name: str)

Will find data block with given name in given collection (bpy.data.mesh, bpy.data.materials ,…) Don't use with objects collection If block does not exist new one will be created

Expand source code
def pick_create_data_block(collection: bpy.types.bpy_prop_collection, block_name: str):
    """
    Will find data block with given name in given collection (bpy.data.mesh, bpy.data.materials ,...)
    Don't use with objects collection
    If block does not exist new one will be created
     """
    block = collection.get(block_name)
    if not block:
        block = collection.new(name=block_name)
    return block
def pick_create_object(obj_name: str, data_block)

Find object with given name, if does not exist will create new object with given data bloc

Expand source code
def pick_create_object(obj_name: str, data_block):
    """Find object with given name, if does not exist will create new object with given data bloc"""
    block = bpy.data.objects.get(obj_name)
    if not block:
        block = bpy.data.objects.new(name=obj_name, object_data=data_block)
    return block

Classes

class BPYPointers (value, names=None, *, module=None, qualname=None, type=None, start=1)

Pointer types which are used in Sverchok New properties should be added with updating collection property

Expand source code
class BPYPointers(Enum):
    """
    Pointer types which are used in Sverchok
    New properties should be added with updating collection property
    """
    # pointer name = type of data
    OBJECT = bpy.types.Object
    MESH = bpy.types.Mesh
    NODE_TREE = bpy.types.NodeTree
    NODE = bpy.types.Node  # there is pointers to nodes in Blender like node.parent property
    MATERIAL = bpy.types.Material
    COLLECTION = bpy.types.Collection
    TEXT = bpy.types.Text
    LIGHT = bpy.types.Light
    IMAGE = bpy.types.Image
    TEXTURE = bpy.types.Texture
    VECTOR_FONT = bpy.types.VectorFont
    GREASE_PENCIL = bpy.types.GreasePencil

    @property
    def collection(self):
        """Map of pointer type and its collection"""
        collections = {
            BPYPointers.OBJECT: bpy.data.objects,
            BPYPointers.MESH: bpy.data.meshes,
            BPYPointers.NODE_TREE: bpy.data.node_groups,
            BPYPointers.NODE: None,
            BPYPointers.MATERIAL:  bpy.data.materials,
            BPYPointers.COLLECTION: bpy.data.collections,
            BPYPointers.TEXT: bpy.data.texts,
            BPYPointers.LIGHT: bpy.data.lights,
            BPYPointers.IMAGE: bpy.data.images,
            BPYPointers.TEXTURE: bpy.data.textures,
            BPYPointers.VECTOR_FONT: bpy.data.curves,
            BPYPointers.GREASE_PENCIL: bpy.data.grease_pencils
        }
        return collections[self]

    @property
    def type(self):
        """Return Blender type of the pointer"""
        return self.value

    @classmethod
    def get_type(cls, bl_rna) -> Union[BPYPointers, None]:
        """Return Python pointer corresponding to given Blender pointer class (bpy.types.Mesh.bl_rna)"""
        for pointer in BPYPointers:
            if pointer.type.bl_rna == bl_rna or pointer.type.bl_rna == bl_rna.base:
                return pointer
        raise TypeError(f'Type: "{bl_rna}" was not found in: {[t.type.bl_rna for t in BPYPointers]}')

Ancestors

  • enum.Enum

Class variables

var COLLECTION
var GREASE_PENCIL
var IMAGE
var LIGHT
var MATERIAL
var MESH
var NODE
var NODE_TREE
var OBJECT
var TEXT
var TEXTURE
var VECTOR_FONT

Static methods

def get_type(bl_rna) ‑> Optional[BPYPointers]

Return Python pointer corresponding to given Blender pointer class (bpy.types.Mesh.bl_rna)

Expand source code
@classmethod
def get_type(cls, bl_rna) -> Union[BPYPointers, None]:
    """Return Python pointer corresponding to given Blender pointer class (bpy.types.Mesh.bl_rna)"""
    for pointer in BPYPointers:
        if pointer.type.bl_rna == bl_rna or pointer.type.bl_rna == bl_rna.base:
            return pointer
    raise TypeError(f'Type: "{bl_rna}" was not found in: {[t.type.bl_rna for t in BPYPointers]}')

Instance variables

var collection

Map of pointer type and its collection

Expand source code
@property
def collection(self):
    """Map of pointer type and its collection"""
    collections = {
        BPYPointers.OBJECT: bpy.data.objects,
        BPYPointers.MESH: bpy.data.meshes,
        BPYPointers.NODE_TREE: bpy.data.node_groups,
        BPYPointers.NODE: None,
        BPYPointers.MATERIAL:  bpy.data.materials,
        BPYPointers.COLLECTION: bpy.data.collections,
        BPYPointers.TEXT: bpy.data.texts,
        BPYPointers.LIGHT: bpy.data.lights,
        BPYPointers.IMAGE: bpy.data.images,
        BPYPointers.TEXTURE: bpy.data.textures,
        BPYPointers.VECTOR_FONT: bpy.data.curves,
        BPYPointers.GREASE_PENCIL: bpy.data.grease_pencils
    }
    return collections[self]
var type

Return Blender type of the pointer

Expand source code
@property
def type(self):
    """Return Blender type of the pointer"""
    return self.value
class BPYProperty (data, prop_name: str)

Wrapper over any property to get access to advance information

Data block can be any blender data like, node, trees, sockets Use with caution as far it keeps straight reference to Blender object

Expand source code
class BPYProperty:
    """Wrapper over any property to get access to advance information"""
    def __init__(self, data, prop_name: str):
        """
        Data block can be any blender data like, node, trees, sockets
        Use with caution as far it keeps straight reference to Blender object
        """
        self.name = prop_name
        self._data = data

    @property
    def is_valid(self) -> bool:
        """
        If data does not have property with given name property is invalid
        It can be so that data.keys() or data.items() can give names of properties which are not in data class any more
        Such properties cab consider as deprecated
        """
        return self.name in self._data.bl_rna.properties

    @property
    def value(self) -> Any:
        """Returns value of the property in Python format, list of dicts for collection, tuple for array"""
        if not self.is_valid:
            raise TypeError(f'Can not read "value" of invalid property "{self.name}"')
        elif self.is_array_like:
            return tuple(getattr(self._data, self.name))
        elif self.type == 'COLLECTION':
            return self._extract_collection_values()
        elif self.type == 'POINTER':  # it simply returns name of data block or None
            data_block = getattr(self._data, self.name)
            return data_block.name if data_block is not None else None
        else:
            return getattr(self._data, self.name)

    @value.setter
    def value(self, value):
        """Apply values in python format to the property"""
        if not self.is_valid:
            raise TypeError(f'Can not read "value" of invalid property "{self.name}"')
        if self.type == 'COLLECTION':
            self._set_collection_values(value)
        elif self.type == 'POINTER' and isinstance(value, str):
            setattr(self._data, self.name, self.data_collection.get(value))
        elif self.type == 'ENUM' and self.is_array_like:
            setattr(self._data, self.name, set(value))
        else:
            setattr(self._data, self.name, value)

    @property
    def type(self) -> str:
        """Type of property: STRING, FLOAT, INT, POINTER, COLLECTION"""
        if not self.is_valid:
            raise TypeError(f'Can not read "type" of invalid property "{self.name}"')
        return self._data.bl_rna.properties[self.name].type

    @property
    def pointer_type(self) -> BPYPointers:
        if self.type != 'POINTER':
            raise TypeError(f'This property is only valid for "POINTER" types, {self.type} type is given')
        return BPYPointers.get_type(self._data.bl_rna)

    @property
    def default_value(self) -> Any:
        """Returns default value, None for pointers, list of dicts of default values for collections"""
        if not self.is_valid:
            raise TypeError(f'Can not read "default_value" of invalid property "{self.name}"')
        elif self.type == 'COLLECTION':
            return self._extract_collection_values(default_value=True)
        elif self.is_array_like:
            if self.type == 'ENUM':
                return tuple(self._data.bl_rna.properties[self.name].default_flag)
            else:
                return tuple(self._data.bl_rna.properties[self.name].default_array)
        elif self.type == 'POINTER':
            return None
        else:
            return self._data.bl_rna.properties[self.name].default

    @property
    def pointer_type(self) -> BPYPointers:
        """It returns subtypes of POINTER type"""
        if self.type != 'POINTER':
            raise TypeError(f'Only POINTER property type has `pointer_type` attribute, "{self.type}" given')
        return BPYPointers.get_type(self._data.bl_rna.properties[self.name].fixed_type)

    @property
    def data_collection(self):
        """For pointer properties only, if pointer type is MESH it will return bpy.data.meshes"""
        return self.pointer_type.collection

    @property
    def is_to_save(self) -> bool:
        """False if property has option BoolProperty(options={'SKIP_SAVE'})"""
        if not self.is_valid:
            raise TypeError(f'Can not read "is_to_save" of invalid property "{self.name}"')
        return not self._data.bl_rna.properties[self.name].is_skip_save

    @property
    def is_array_like(self) -> bool:
        """True for VectorArray, FloatArray, IntArray, Enum with enum flag"""
        if not self.is_valid:
            raise TypeError(f'Can not read "is_array_like" of invalid property "{self.name}"')
        if self.type in {'BOOLEAN', 'FLOAT', 'INT'}:
            return self._data.bl_rna.properties[self.name].is_array
        elif self.type == 'ENUM':
            # Enum can return set of values, array like
            return self._data.bl_rna.properties[self.name].is_enum_flag
        else:
            # other properties does not have is_array attribute
            return False

    def unset(self):
        """Assign default value to the property"""
        self._data.property_unset(self.name)

    def filter_collection_values(self, skip_default=True, skip_save=True):
        """Convert data of collection property into python format with skipping certain properties"""
        if self.type != 'COLLECTION':
            raise TypeError(f'Method supported only "collection" types, "{self.type}" was given')
        if not self.is_valid:
            raise TypeError(f'Can not read "non default collection values" of invalid property "{self.name}"')
        items = []
        for item in getattr(self._data, self.name):
            item_props = {}

            # in some nodes collections are getting just PropertyGroup type instead of its subclasses
            # PropertyGroup itself does not have any properties
            item_properties = item.__annotations__ if hasattr(item, '__annotations__') else []
            for prop_name in chain(['name'], item_properties):  # item.items() will return only changed values
                prop = BPYProperty(item, prop_name)
                if not prop.is_valid:
                    continue
                if skip_save and not prop.is_to_save:
                    continue
                if prop.type != 'COLLECTION':
                    if skip_default and prop.default_value == prop.value:
                        continue
                    item_props[prop.name] = prop.value
                else:
                    item_props[prop.name] = prop.filter_collection_values(skip_default, skip_save)
            items.append(item_props)
        return items

    def collection_to_list(self):
        """Returns data structure like this [[p1, p2, p3], [p4, p5, p6]]
        in this example the collection has two items, each item has 3 properties"""
        if self.type != 'COLLECTION':
            raise TypeError(f'Method supported only "collection" types, "{self.type}" was given')
        if not self.is_valid:
            raise TypeError(f'Can not read "non default collection values" of invalid property "{self.name}"')

        collection = []
        for item in getattr(self._data, self.name):
            prop_list = []
            # in some nodes collections are getting just PropertyGroup type instead of its subclasses
            # PropertyGroup itself does not have any properties
            item_properties = item.__annotations__ if hasattr(item, '__annotations__') else []
            for prop_name in chain(['name'], item_properties):  # item.items() will return only changed values
                prop = BPYProperty(item, prop_name)
                prop_list.append(prop)
            collection.append(prop_list)
        return collection

    def _extract_collection_values(self, default_value: bool = False):
        """returns something like this: [{"name": "", "my_prop": 1.0}, {"name": "", "my_prop": 2.0}, ...]"""
        items = []
        for item in getattr(self._data, self.name):
            item_props = {}

            # in some nodes collections are getting just PropertyGroup type instead of its subclasses
            # PropertyGroup itself does not have any properties
            item_properties = item.__annotations__ if hasattr(item, '__annotations__') else []
            for prop_name in chain(['name'], item_properties):  # item.items() will return only changed values
                prop = BPYProperty(item, prop_name)
                if prop.is_valid:
                    item_props[prop.name] = prop.default_value if default_value else prop.value
            items.append(item_props)
        return items

    def _set_collection_values(self, value: List[dict]):
        """Assign Python data to collection property"""
        collection = getattr(self._data, self.name)
        for item_index, item_values in enumerate(value):
            # Some collections can be empty, in this case they should be expanded to be able to get new values
            if item_index == len(collection):
                item = collection.add()
            else:
                item = collection[item_index]
            for prop_name, prop_value in item_values.items():
                prop = BPYProperty(item, prop_name)
                if prop.is_valid:
                    prop.value = prop_value

Instance variables

var data_collection

For pointer properties only, if pointer type is MESH it will return bpy.data.meshes

Expand source code
@property
def data_collection(self):
    """For pointer properties only, if pointer type is MESH it will return bpy.data.meshes"""
    return self.pointer_type.collection
var default_value : Any

Returns default value, None for pointers, list of dicts of default values for collections

Expand source code
@property
def default_value(self) -> Any:
    """Returns default value, None for pointers, list of dicts of default values for collections"""
    if not self.is_valid:
        raise TypeError(f'Can not read "default_value" of invalid property "{self.name}"')
    elif self.type == 'COLLECTION':
        return self._extract_collection_values(default_value=True)
    elif self.is_array_like:
        if self.type == 'ENUM':
            return tuple(self._data.bl_rna.properties[self.name].default_flag)
        else:
            return tuple(self._data.bl_rna.properties[self.name].default_array)
    elif self.type == 'POINTER':
        return None
    else:
        return self._data.bl_rna.properties[self.name].default
var is_array_like : bool

True for VectorArray, FloatArray, IntArray, Enum with enum flag

Expand source code
@property
def is_array_like(self) -> bool:
    """True for VectorArray, FloatArray, IntArray, Enum with enum flag"""
    if not self.is_valid:
        raise TypeError(f'Can not read "is_array_like" of invalid property "{self.name}"')
    if self.type in {'BOOLEAN', 'FLOAT', 'INT'}:
        return self._data.bl_rna.properties[self.name].is_array
    elif self.type == 'ENUM':
        # Enum can return set of values, array like
        return self._data.bl_rna.properties[self.name].is_enum_flag
    else:
        # other properties does not have is_array attribute
        return False
var is_to_save : bool

False if property has option BoolProperty(options={'SKIP_SAVE'})

Expand source code
@property
def is_to_save(self) -> bool:
    """False if property has option BoolProperty(options={'SKIP_SAVE'})"""
    if not self.is_valid:
        raise TypeError(f'Can not read "is_to_save" of invalid property "{self.name}"')
    return not self._data.bl_rna.properties[self.name].is_skip_save
var is_valid : bool

If data does not have property with given name property is invalid It can be so that data.keys() or data.items() can give names of properties which are not in data class any more Such properties cab consider as deprecated

Expand source code
@property
def is_valid(self) -> bool:
    """
    If data does not have property with given name property is invalid
    It can be so that data.keys() or data.items() can give names of properties which are not in data class any more
    Such properties cab consider as deprecated
    """
    return self.name in self._data.bl_rna.properties
var pointer_typeBPYPointers

It returns subtypes of POINTER type

Expand source code
@property
def pointer_type(self) -> BPYPointers:
    """It returns subtypes of POINTER type"""
    if self.type != 'POINTER':
        raise TypeError(f'Only POINTER property type has `pointer_type` attribute, "{self.type}" given')
    return BPYPointers.get_type(self._data.bl_rna.properties[self.name].fixed_type)
var type : str

Type of property: STRING, FLOAT, INT, POINTER, COLLECTION

Expand source code
@property
def type(self) -> str:
    """Type of property: STRING, FLOAT, INT, POINTER, COLLECTION"""
    if not self.is_valid:
        raise TypeError(f'Can not read "type" of invalid property "{self.name}"')
    return self._data.bl_rna.properties[self.name].type
var value : Any

Returns value of the property in Python format, list of dicts for collection, tuple for array

Expand source code
@property
def value(self) -> Any:
    """Returns value of the property in Python format, list of dicts for collection, tuple for array"""
    if not self.is_valid:
        raise TypeError(f'Can not read "value" of invalid property "{self.name}"')
    elif self.is_array_like:
        return tuple(getattr(self._data, self.name))
    elif self.type == 'COLLECTION':
        return self._extract_collection_values()
    elif self.type == 'POINTER':  # it simply returns name of data block or None
        data_block = getattr(self._data, self.name)
        return data_block.name if data_block is not None else None
    else:
        return getattr(self._data, self.name)

Methods

def collection_to_list(self)

Returns data structure like this [[p1, p2, p3], [p4, p5, p6]] in this example the collection has two items, each item has 3 properties

Expand source code
def collection_to_list(self):
    """Returns data structure like this [[p1, p2, p3], [p4, p5, p6]]
    in this example the collection has two items, each item has 3 properties"""
    if self.type != 'COLLECTION':
        raise TypeError(f'Method supported only "collection" types, "{self.type}" was given')
    if not self.is_valid:
        raise TypeError(f'Can not read "non default collection values" of invalid property "{self.name}"')

    collection = []
    for item in getattr(self._data, self.name):
        prop_list = []
        # in some nodes collections are getting just PropertyGroup type instead of its subclasses
        # PropertyGroup itself does not have any properties
        item_properties = item.__annotations__ if hasattr(item, '__annotations__') else []
        for prop_name in chain(['name'], item_properties):  # item.items() will return only changed values
            prop = BPYProperty(item, prop_name)
            prop_list.append(prop)
        collection.append(prop_list)
    return collection
def filter_collection_values(self, skip_default=True, skip_save=True)

Convert data of collection property into python format with skipping certain properties

Expand source code
def filter_collection_values(self, skip_default=True, skip_save=True):
    """Convert data of collection property into python format with skipping certain properties"""
    if self.type != 'COLLECTION':
        raise TypeError(f'Method supported only "collection" types, "{self.type}" was given')
    if not self.is_valid:
        raise TypeError(f'Can not read "non default collection values" of invalid property "{self.name}"')
    items = []
    for item in getattr(self._data, self.name):
        item_props = {}

        # in some nodes collections are getting just PropertyGroup type instead of its subclasses
        # PropertyGroup itself does not have any properties
        item_properties = item.__annotations__ if hasattr(item, '__annotations__') else []
        for prop_name in chain(['name'], item_properties):  # item.items() will return only changed values
            prop = BPYProperty(item, prop_name)
            if not prop.is_valid:
                continue
            if skip_save and not prop.is_to_save:
                continue
            if prop.type != 'COLLECTION':
                if skip_default and prop.default_value == prop.value:
                    continue
                item_props[prop.name] = prop.value
            else:
                item_props[prop.name] = prop.filter_collection_values(skip_default, skip_save)
        items.append(item_props)
    return items
def unset(self)

Assign default value to the property

Expand source code
def unset(self):
    """Assign default value to the property"""
    self._data.property_unset(self.name)
class BlDomains (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class BlDomains(Enum):
    # don't change the order - new items add to the end
    POINT = 'Point'
    EDGE = 'Edge'
    FACE = 'Face'
    CORNER = 'Face Corner'

    # It does not have sense to include these attributes because there is no API
    # for generating instances and applying attributes to a curve.
    # CURVE = 'Spline'
    # INSTANCE = 'Instance'

Ancestors

  • enum.Enum

Class variables

var CORNER
var EDGE
var FACE
var POINT
class BlModifier (modifier)
Expand source code
class BlModifier:
    def __init__(self, modifier):
        self._mod: bpy.types.Modifier = modifier
        self.gn_tree: Optional[BlTree] = None  # cache for performance

    @property
    def node_group(self):
        return getattr(self._mod, 'node_group', None)

    @node_group.setter
    def node_group(self, node_group):
        self._mod.node_group = node_group

    def get_property(self, name):
        return getattr(self._mod, name)

    def set_property(self, name, value):
        setattr(self._mod, name, value)

    def get_tree_prop(self, name):
        return self._mod[name]

    def set_tree_prop(self, name, value):
        """Good for coping properties from one modifier to another"""
        self._mod[name] = value

    def set_tree_data(self, name, data, domain='POINT'):
        """Transfer py data to node modifier tree"""

        # transfer single value
        if not isinstance(data, (list, tuple)):
            data = [data]
        if not self.gn_tree.is_field(name) and len(data) != 1:
            data = data[:1]
        if len(data) == 1:
            value = data[0]
            self._mod[f"{name}_use_attribute"] = 0
            if isinstance(value, (list, tuple)):  # list of single vertex
                # for some reason node modifier can't apply python sequences directly
                for i, v in enumerate(value):
                    self._mod[name][i] = v
            else:
                sock = self.gn_tree.inputs[name]
                if sock.type in {'INT', 'BOOLEAN'}:
                    value = int(value)
                elif sock.type == 'VALUE':
                    value = float(value)
                elif sock.type == 'STRING':
                    value = str(value)
                self._mod[name] = value

        # transfer field
        else:
            self._mod[f"{name}_use_attribute"] = 1
            self._mod[f"{name}_attribute_name"] = name
            obj = BlObject(self._mod.id_data)
            sock = self.gn_tree.inputs[name]
            if sock.type in {'INT', 'BOOLEAN'} and not isinstance(data[0], int):
                data = [int(i) for i in data]
            bl_sock = BlSocket(sock)
            obj.set_attribute(data, name, domain, value_type=bl_sock.attribute_type)

    def remove(self):
        obj = self._mod.id_data
        obj.modifiers.remove(self._mod)
        self._mod = None

    @property
    def type(self) -> str:
        return self._mod.type

    def __eq__(self, other):
        if isinstance(other, BlModifier):
            # check type
            if self.type != other.type:
                return False

            # check properties
            for prop in (p for p in self._mod.bl_rna.properties if not p.is_readonly):
                if other.get_property(prop.identifier) != self.get_property(prop.identifier):
                    return False

            # check tree properties
            if self._mod.type == 'NODES' and self._mod.node_group:
                for tree_inp in self._mod.node_group.inputs[1:]:
                    prop_name = tree_inp.identifier
                    if self.get_tree_prop(prop_name) != other.get_tree_prop(prop_name):
                        return False
                    use_name = f"{prop_name}_use_attribute"
                    if self.get_tree_prop(use_name) != other.get_tree_prop(use_name):
                        return False
                    attr_name = f"{prop_name}_attribute_name"
                    if self.get_tree_prop(attr_name) != other.get_tree_prop(attr_name):
                        return False
                for tree_out in self._mod.node_group.outputs[1:]:
                    prop_name = f"{tree_out.identifier}_attribute_name"
                    if self.get_tree_prop(prop_name) != other.get_tree_prop(prop_name):
                        return False

            return True
        else:
            return NotImplemented

Instance variables

var node_group
Expand source code
@property
def node_group(self):
    return getattr(self._mod, 'node_group', None)
var type : str
Expand source code
@property
def type(self) -> str:
    return self._mod.type

Methods

def get_property(self, name)
Expand source code
def get_property(self, name):
    return getattr(self._mod, name)
def get_tree_prop(self, name)
Expand source code
def get_tree_prop(self, name):
    return self._mod[name]
def remove(self)
Expand source code
def remove(self):
    obj = self._mod.id_data
    obj.modifiers.remove(self._mod)
    self._mod = None
def set_property(self, name, value)
Expand source code
def set_property(self, name, value):
    setattr(self._mod, name, value)
def set_tree_data(self, name, data, domain='POINT')

Transfer py data to node modifier tree

Expand source code
def set_tree_data(self, name, data, domain='POINT'):
    """Transfer py data to node modifier tree"""

    # transfer single value
    if not isinstance(data, (list, tuple)):
        data = [data]
    if not self.gn_tree.is_field(name) and len(data) != 1:
        data = data[:1]
    if len(data) == 1:
        value = data[0]
        self._mod[f"{name}_use_attribute"] = 0
        if isinstance(value, (list, tuple)):  # list of single vertex
            # for some reason node modifier can't apply python sequences directly
            for i, v in enumerate(value):
                self._mod[name][i] = v
        else:
            sock = self.gn_tree.inputs[name]
            if sock.type in {'INT', 'BOOLEAN'}:
                value = int(value)
            elif sock.type == 'VALUE':
                value = float(value)
            elif sock.type == 'STRING':
                value = str(value)
            self._mod[name] = value

    # transfer field
    else:
        self._mod[f"{name}_use_attribute"] = 1
        self._mod[f"{name}_attribute_name"] = name
        obj = BlObject(self._mod.id_data)
        sock = self.gn_tree.inputs[name]
        if sock.type in {'INT', 'BOOLEAN'} and not isinstance(data[0], int):
            data = [int(i) for i in data]
        bl_sock = BlSocket(sock)
        obj.set_attribute(data, name, domain, value_type=bl_sock.attribute_type)
def set_tree_prop(self, name, value)

Good for coping properties from one modifier to another

Expand source code
def set_tree_prop(self, name, value):
    """Good for coping properties from one modifier to another"""
    self._mod[name] = value
class BlNode (node)

Wrapping around ordinary node for extracting some its information

Expand source code
class BlNode:
    """Wrapping around ordinary node for extracting some its information"""
    DEBUG_NODES_IDS = {'SvDebugPrintNode', 'SvStethoscopeNode'}  # can be added as Mix-in class

    def __init__(self, node):
        self.data = node

    @property
    def properties(self) -> List[BPYProperty]:
        """Iterator over all node properties"""
        node_properties = self.data.bl_rna.__annotations__ if hasattr(self.data.bl_rna, '__annotations__') else []
        return [BPYProperty(self.data, prop_name) for prop_name in node_properties]

    @property
    def is_debug_node(self) -> bool:
        """Nodes which print sockets content"""
        return self.base_idname in self.DEBUG_NODES_IDS

    @property
    def base_idname(self) -> str:
        """SvStethoscopeNodeMK2 -> SvStethoscopeNode
        it won't parse more tricky variants like SvStethoscopeMK2Node which I saw exists"""
        id_name, _, version = self.data.bl_idname.partition('MK')
        try:
            int(version)
        except ValueError:
            return self.data.bl_idname
        return id_name

Class variables

var DEBUG_NODES_IDS

Instance variables

var base_idname : str

SvStethoscopeNodeMK2 -> SvStethoscopeNode it won't parse more tricky variants like SvStethoscopeMK2Node which I saw exists

Expand source code
@property
def base_idname(self) -> str:
    """SvStethoscopeNodeMK2 -> SvStethoscopeNode
    it won't parse more tricky variants like SvStethoscopeMK2Node which I saw exists"""
    id_name, _, version = self.data.bl_idname.partition('MK')
    try:
        int(version)
    except ValueError:
        return self.data.bl_idname
    return id_name
var is_debug_node : bool

Nodes which print sockets content

Expand source code
@property
def is_debug_node(self) -> bool:
    """Nodes which print sockets content"""
    return self.base_idname in self.DEBUG_NODES_IDS
var properties : List[BPYProperty]

Iterator over all node properties

Expand source code
@property
def properties(self) -> List[BPYProperty]:
    """Iterator over all node properties"""
    node_properties = self.data.bl_rna.__annotations__ if hasattr(self.data.bl_rna, '__annotations__') else []
    return [BPYProperty(self.data, prop_name) for prop_name in node_properties]
class BlObject (obj)
Expand source code
class BlObject:
    def __init__(self, obj):
        self._obj: bpy.types.Object = obj

    def set_attribute(self, values, attr_name, domain='POINT', value_type='FLOAT'):
        obj = self._obj
        attr = obj.data.attributes.get(attr_name)
        if attr is None:
            attr = obj.data.attributes.new(attr_name, value_type, domain)
        elif attr.data_type != value_type or attr.domain != domain:
            obj.data.attributes.remove(attr)
            attr = obj.data.attributes.new(attr_name, value_type, domain)

        if domain == 'POINT':
            amount = len(obj.data.vertices)
        elif domain == 'EDGE':
            amount = len(obj.data.edges)
        elif domain == 'CORNER':
            amount = len(obj.data.loops)
        elif domain == 'FACE':
            amount = len(obj.data.polygons)
        else:
            raise TypeError(f'Unsupported domain {domain}')

        if value_type in ['FLOAT', 'INT', 'BOOLEAN']:
            data = list(fixed_iter(values, amount))
        elif value_type in ['FLOAT_VECTOR', 'FLOAT_COLOR']:
            data = [co for v in fixed_iter(values, amount) for co in v]
        elif value_type == 'FLOAT2':
            data = [co for v in fixed_iter(values, amount) for co in v[:2]]
        else:
            raise TypeError(f'Unsupported type {value_type}')

        if value_type in ["FLOAT", "INT", "BOOLEAN"]:
            attr.data.foreach_set("value", data)
        elif value_type in ["FLOAT_VECTOR", "FLOAT2"]:
            attr.data.foreach_set("vector", data)
        else:
            attr.data.foreach_set("color", data)

        # attr.data.update()

Methods

def set_attribute(self, values, attr_name, domain='POINT', value_type='FLOAT')
Expand source code
def set_attribute(self, values, attr_name, domain='POINT', value_type='FLOAT'):
    obj = self._obj
    attr = obj.data.attributes.get(attr_name)
    if attr is None:
        attr = obj.data.attributes.new(attr_name, value_type, domain)
    elif attr.data_type != value_type or attr.domain != domain:
        obj.data.attributes.remove(attr)
        attr = obj.data.attributes.new(attr_name, value_type, domain)

    if domain == 'POINT':
        amount = len(obj.data.vertices)
    elif domain == 'EDGE':
        amount = len(obj.data.edges)
    elif domain == 'CORNER':
        amount = len(obj.data.loops)
    elif domain == 'FACE':
        amount = len(obj.data.polygons)
    else:
        raise TypeError(f'Unsupported domain {domain}')

    if value_type in ['FLOAT', 'INT', 'BOOLEAN']:
        data = list(fixed_iter(values, amount))
    elif value_type in ['FLOAT_VECTOR', 'FLOAT_COLOR']:
        data = [co for v in fixed_iter(values, amount) for co in v]
    elif value_type == 'FLOAT2':
        data = [co for v in fixed_iter(values, amount) for co in v[:2]]
    else:
        raise TypeError(f'Unsupported type {value_type}')

    if value_type in ["FLOAT", "INT", "BOOLEAN"]:
        attr.data.foreach_set("value", data)
    elif value_type in ["FLOAT_VECTOR", "FLOAT2"]:
        attr.data.foreach_set("vector", data)
    else:
        attr.data.foreach_set("color", data)

    # attr.data.update()
class BlSocket (socket)
Expand source code
class BlSocket:
    _attr_types = {
        'VECTOR': 'FLOAT_VECTOR',
        'VALUE': 'FLOAT',
        'RGBA': 'FLOAT_COLOR',
        'INT': 'INT',
        'BOOLEAN': 'BOOLEAN',
    }

    _sv_types = {
        'VECTOR': 'SvVerticesSocket',
        'VALUE': 'SvStringsSocket',
        'RGBA': 'SvColorSocket',
        'INT': 'SvStringsSocket',
        'STRING': 'SvTextSocket',
        'BOOLEAN': 'SvStringsSocket',
        'OBJECT': 'SvObjectSocket',
        'COLLECTION': 'SvCollectionSocket',
        'MATERIAL': 'SvMaterialSocket',
        'TEXTURE': 'SvTextureSocket',
        'IMAGE': 'SvImageSocket',
    }

    def __init__(self, socket):
        self._sock: bpy.types.NodeSocket = socket

    def copy_properties(self, sv_sock):
        sv_sock.name = self._sock.name

        if sv_sock.bl_idname == 'SvStringsSocket':
            if self._sock.type == 'VALUE':
                sv_sock.default_property_type = 'float'
            elif self._sock.type in {'INT', 'BOOLEAN'}:
                sv_sock.default_property_type = 'int'
            else:
                return  # There is no default property for such type
            if sv_sock.default_property == 0:  # was unchanged by user
                sv_sock.default_property = self._sock.default_value
                sv_sock.use_prop = True

        elif sv_sock.bl_idname == 'SvVerticesSocket':
            if sv_sock.default_property[:] == (0, 0, 0):  # was unchanged by user
                sv_sock.default_property = self._sock.default_value
                sv_sock.use_prop = True

        elif sv_sock.bl_idname == 'SvObjectSocket':
            if sv_sock.default_property is None:  # was unchanged by user
                sv_sock.object_ref_pointer = self._sock.default_value
                sv_sock.use_prop = True

        elif hasattr(sv_sock, 'default_property'):
            sv_default = BPYProperty(sv_sock, 'default_property').default_value
            if isinstance(sv_sock.default_property, bpy.types.bpy_prop_array):
                current = sv_sock.default_property[:]
            else:
                current = sv_sock.default_property
            if sv_default != current:
                return  # the value was already changed by user
            sv_sock.default_property = self._sock.default_value
            sv_sock.use_prop = True

    @classmethod
    def from_identifier(cls, sockets, identifier):
        for s in sockets:
            if s.identifier == identifier:
                return cls(s)
        raise LookupError(f"Socket with {identifier=} was not found")

    @property
    def attribute_type(self):
        return self._attr_types[self._sock.type]

    @property
    def sverchok_type(self):
        if (sv_type := self._sv_types.get(self._sock.type)) is None:
            return 'SvStringsSocket'
        return sv_type

    @property
    def display_shape(self):
        return self._sock.display_shape

Static methods

def from_identifier(sockets, identifier)
Expand source code
@classmethod
def from_identifier(cls, sockets, identifier):
    for s in sockets:
        if s.identifier == identifier:
            return cls(s)
    raise LookupError(f"Socket with {identifier=} was not found")

Instance variables

var attribute_type
Expand source code
@property
def attribute_type(self):
    return self._attr_types[self._sock.type]
var display_shape
Expand source code
@property
def display_shape(self):
    return self._sock.display_shape
var sverchok_type
Expand source code
@property
def sverchok_type(self):
    if (sv_type := self._sv_types.get(self._sock.type)) is None:
        return 'SvStringsSocket'
    return sv_type

Methods

def copy_properties(self, sv_sock)
Expand source code
def copy_properties(self, sv_sock):
    sv_sock.name = self._sock.name

    if sv_sock.bl_idname == 'SvStringsSocket':
        if self._sock.type == 'VALUE':
            sv_sock.default_property_type = 'float'
        elif self._sock.type in {'INT', 'BOOLEAN'}:
            sv_sock.default_property_type = 'int'
        else:
            return  # There is no default property for such type
        if sv_sock.default_property == 0:  # was unchanged by user
            sv_sock.default_property = self._sock.default_value
            sv_sock.use_prop = True

    elif sv_sock.bl_idname == 'SvVerticesSocket':
        if sv_sock.default_property[:] == (0, 0, 0):  # was unchanged by user
            sv_sock.default_property = self._sock.default_value
            sv_sock.use_prop = True

    elif sv_sock.bl_idname == 'SvObjectSocket':
        if sv_sock.default_property is None:  # was unchanged by user
            sv_sock.object_ref_pointer = self._sock.default_value
            sv_sock.use_prop = True

    elif hasattr(sv_sock, 'default_property'):
        sv_default = BPYProperty(sv_sock, 'default_property').default_value
        if isinstance(sv_sock.default_property, bpy.types.bpy_prop_array):
            current = sv_sock.default_property[:]
        else:
            current = sv_sock.default_property
        if sv_default != current:
            return  # the value was already changed by user
        sv_sock.default_property = self._sock.default_value
        sv_sock.use_prop = True
class BlSockets (sockets: Union[NodeInputs, NodeOutputs])
Expand source code
class BlSockets:
    def __init__(self, sockets: Union[NodeInputs, NodeOutputs]):
        self._sockets = sockets

    def copy_sockets(self, sockets_from: Iterable):
        """Copy sockets from one collection to another. Also, it can be used
        to refresh `to` collection to be equal to `from` collection and in this
        case only new socket will be added and old one removed.
        It can copy properties:
        sv socket -> sv socket
        sv interface socket -> sv socket
        """
        sockets_to = self._sockets
        # remove sockets which are not presented in from collection
        identifiers_from = {s.identifier for s in sockets_from}
        for s_to in sockets_to:
            if s_to.identifier not in identifiers_from:
                sockets_to.remove(s_to)

        # add new sockets
        sock_indexes_to = {s.identifier: i for i, s in enumerate(sockets_to)}
        for s_from in sockets_from:
            if s_from.identifier in sock_indexes_to:
                continue
            id_name = getattr(s_from, 'bl_socket_idname', s_from.bl_idname)
            s_to = sockets_to.new(id_name, s_from.name, identifier=s_from.identifier)
            sock_indexes_to[s_to.identifier] = len(sockets_to) - 1

        # fix existing sockets
        for s_from in sockets_from:
            s_to = sockets_to[sock_indexes_to[s_from.identifier]]
            id_name = getattr(s_from, 'bl_socket_idname', s_from.bl_idname)
            if id_name != s_to.bl_idname:
                s_to = s_to.replace_socket(id_name)

        # fix socket positions
        for new_pos, s_from in enumerate(sockets_from):
            current_pos = sock_indexes_to[s_from.identifier]
            if current_pos != new_pos:
                sockets_to.move(current_pos, new_pos)
                sock_indexes_to = {
                    s.identifier: i for i, s in enumerate(sockets_to)}

Methods

def copy_sockets(self, sockets_from: Iterable)

Copy sockets from one collection to another. Also, it can be used to refresh to collection to be equal to from collection and in this case only new socket will be added and old one removed. It can copy properties: sv socket -> sv socket sv interface socket -> sv socket

Expand source code
def copy_sockets(self, sockets_from: Iterable):
    """Copy sockets from one collection to another. Also, it can be used
    to refresh `to` collection to be equal to `from` collection and in this
    case only new socket will be added and old one removed.
    It can copy properties:
    sv socket -> sv socket
    sv interface socket -> sv socket
    """
    sockets_to = self._sockets
    # remove sockets which are not presented in from collection
    identifiers_from = {s.identifier for s in sockets_from}
    for s_to in sockets_to:
        if s_to.identifier not in identifiers_from:
            sockets_to.remove(s_to)

    # add new sockets
    sock_indexes_to = {s.identifier: i for i, s in enumerate(sockets_to)}
    for s_from in sockets_from:
        if s_from.identifier in sock_indexes_to:
            continue
        id_name = getattr(s_from, 'bl_socket_idname', s_from.bl_idname)
        s_to = sockets_to.new(id_name, s_from.name, identifier=s_from.identifier)
        sock_indexes_to[s_to.identifier] = len(sockets_to) - 1

    # fix existing sockets
    for s_from in sockets_from:
        s_to = sockets_to[sock_indexes_to[s_from.identifier]]
        id_name = getattr(s_from, 'bl_socket_idname', s_from.bl_idname)
        if id_name != s_to.bl_idname:
            s_to = s_to.replace_socket(id_name)

    # fix socket positions
    for new_pos, s_from in enumerate(sockets_from):
        current_pos = sock_indexes_to[s_from.identifier]
        if current_pos != new_pos:
            sockets_to.move(current_pos, new_pos)
            sock_indexes_to = {
                s.identifier: i for i, s in enumerate(sockets_to)}
class BlTree (tree)
Expand source code
class BlTree:
    def __init__(self, tree):
        self._tree = tree
        self.inputs = {s.identifier: s for s in tree.inputs}
        self.outputs = {s.identifier: s for s in tree.outputs}

        self.is_field = lru_cache(self._is_field)  # for performance

    @cached_property
    def group_input(self):
        for node in self._tree.nodes:
            if node.bl_idname == 'NodeGroupInput':
                return node
        return None

    def _is_field(self, input_socket_identifier):
        """Check whether input tree socket expects field (diamond socket)"""
        if (group := self.group_input) is None:
            raise LookupError(f'Group input node is required '
                              f'which is not found in "{self._tree.name}" tree')
        sock = BlSocket.from_identifier(group.outputs, input_socket_identifier)
        return sock.display_shape == 'DIAMOND'

Instance variables

var group_input
Expand source code
def __get__(self, instance, owner=None):
    if instance is None:
        return self
    if self.attrname is None:
        raise TypeError(
            "Cannot use cached_property instance without calling __set_name__ on it.")
    try:
        cache = instance.__dict__
    except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
        msg = (
            f"No '__dict__' attribute on {type(instance).__name__!r} "
            f"instance to cache {self.attrname!r} property."
        )
        raise TypeError(msg) from None
    val = cache.get(self.attrname, _NOT_FOUND)
    if val is _NOT_FOUND:
        with self.lock:
            # check if another thread filled cache while we awaited lock
            val = cache.get(self.attrname, _NOT_FOUND)
            if val is _NOT_FOUND:
                val = self.func(instance)
                try:
                    cache[self.attrname] = val
                except TypeError:
                    msg = (
                        f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                        f"does not support item assignment for caching {self.attrname!r} property."
                    )
                    raise TypeError(msg) from None
    return val
class BlTrees (node_groups=None)

Wrapping around Blender tree, use with care it can crash if other containers are modified a lot https://docs.blender.org/api/current/info_gotcha.html#help-my-script-crashes-blender All this is True and about Blender class itself

Expand source code
class BlTrees:
    """Wrapping around Blender tree, use with care
    it can crash if other containers are modified a lot
    https://docs.blender.org/api/current/info_gotcha.html#help-my-script-crashes-blender
    All this is True and about Blender class itself"""

    MAIN_TREE_ID = 'SverchCustomTreeType'
    GROUP_ID = 'SvGroupTree'

    def __init__(self, node_groups=None):
        self._trees = node_groups

    @classmethod
    def is_main_tree(cls, tree):
        return tree.bl_idname == cls.MAIN_TREE_ID

    @property
    def sv_trees(self) -> Iterable[Union[SverchCustomTree, SvGroupTree]]:
        """All Sverchok trees in a file or in given set of trees"""
        trees = self._trees or bpy.data.node_groups
        return (t for t in trees if t.bl_idname in [self.MAIN_TREE_ID, self.GROUP_ID])

    @property
    def sv_main_trees(self) -> Iterable[SverchCustomTree]:
        """All main Sverchok trees in a file or in given set of trees"""
        trees = self._trees or bpy.data.node_groups
        return (t for t in trees if t.bl_idname == self.MAIN_TREE_ID)

    @property
    def sv_group_trees(self) -> Iterable[SvGroupTree]:
        """All Sverchok group trees"""
        trees = self._trees or bpy.data.node_groups
        return (t for t in trees if t.bl_idname == self.GROUP_ID)

Class variables

var GROUP_ID
var MAIN_TREE_ID

Static methods

def is_main_tree(tree)
Expand source code
@classmethod
def is_main_tree(cls, tree):
    return tree.bl_idname == cls.MAIN_TREE_ID

Instance variables

var sv_group_trees : Iterable[SvGroupTree]

All Sverchok group trees

Expand source code
@property
def sv_group_trees(self) -> Iterable[SvGroupTree]:
    """All Sverchok group trees"""
    trees = self._trees or bpy.data.node_groups
    return (t for t in trees if t.bl_idname == self.GROUP_ID)
var sv_main_trees : Iterable[SverchCustomTree]

All main Sverchok trees in a file or in given set of trees

Expand source code
@property
def sv_main_trees(self) -> Iterable[SverchCustomTree]:
    """All main Sverchok trees in a file or in given set of trees"""
    trees = self._trees or bpy.data.node_groups
    return (t for t in trees if t.bl_idname == self.MAIN_TREE_ID)
var sv_trees : Iterable[Union[SverchCustomTree, SvGroupTree]]

All Sverchok trees in a file or in given set of trees

Expand source code
@property
def sv_trees(self) -> Iterable[Union[SverchCustomTree, SvGroupTree]]:
    """All Sverchok trees in a file or in given set of trees"""
    trees = self._trees or bpy.data.node_groups
    return (t for t in trees if t.bl_idname in [self.MAIN_TREE_ID, self.GROUP_ID])