diff --git a/core/__init__.py b/core/__init__.py index ec9632cbec..4ded09ed54 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -12,7 +12,7 @@ core_modules = [ "monad_properties", "sv_custom_exceptions", - "node_id_dict", "links", "sockets", + "node_id_dict", "links", "sockets", "socket_data_containers", "handlers", "update_system", "upgrade_nodes", "monad", "events", "node_group", "group_handlers" ] diff --git a/core/socket_data_containers.py b/core/socket_data_containers.py new file mode 100644 index 0000000000..98734efc09 --- /dev/null +++ b/core/socket_data_containers.py @@ -0,0 +1,109 @@ +# 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 + + +""" +The purpose of this module is to keep classes fo socket data containers +Usually such containers are just lists but in trick cases it requires more customization +""" + + +from typing import List, Union, Iterable + +import bpy +from sverchok.utils.handle_blender_data import BPYPointers + + +class SocketData: + """It should posses such method as 'copy', 'wrap', 'unwrap' and etc. later""" + + def get_data(self): + """This method will be called in socket.sv_get method""" + raise NotImplementedError(f'"get_data" method is not implemented in "{type(self).__name__}" class') + + def __len__(self): + """return number of objects for showing in socket names""" + raise NotImplementedError(f'"len" method is not implemented in "{type(self).__name__}" class') + + +class ObjectSocketData(SocketData): + """It will store meta information for refreshing links to given objects""" + + def __init__(self, data: list): + """Data is list of objects or list of lists of objects. Object can be all types of bpy.data""" + self._data = data + + # meta data of the same shape as input data + self._collection_names: Union[List[str], List[list]] = self._apply_func_it(self._get_col_name, data) + self._object_names: Union[List[str], List[list]] = self._apply_func_it(self._get_block_name, data) + + def get_data(self): + """It will refresh references to data if necessary""" + + def refresh_data(col_name, name): + # this potentially can be slow, solution could be create separate class for cashing searches + return getattr(bpy.data, col_name).get(name) + + def first_none_iter(it): + # should be moved somewhere + return first_none_iter(next(iter(it))) if isinstance(it, Iterable) else it + + # assume that either all blocks are invalid or none + if not self._is_valid_reference(first_none_iter(self._data)): + print('Outdated Object data detected - fixing') # todo remove + self._data = self._apply_func_its(refresh_data, self._collection_names, self._object_names) + + return self._data + + @staticmethod + def _get_col_name(block): + return BPYPointers.get_type(block.bl_rna).collection_name + + @staticmethod + def _get_block_name(block): + return block.name + + @staticmethod + def _is_valid_reference(obj) -> bool: + """Test of keeping valid references to a data block, should be moved in another location later""" + try: + obj.name # assume that all given data blocks has name property + return True + except ReferenceError: + return False + + def _apply_func_it(self, func, it: Iterable) -> list: + """Apply function to none iterable elements. Input shape is unchanged. + Should be moved in another module later""" + out = [] + for i in it: + if isinstance(i, Iterable): + out.append(self._apply_func_it(func, i)) + else: + out.append(func(i)) + return out + + def _apply_func_its(self, func, *its: Iterable) -> list: + """ + Apply function to none iterable elements. Input shape is unchanged. + Can get multiple iterables with the same shape. + func should have the same number of arguments as input iterables. + + Should be moved in another module later + _apply_func_its(lambda a, b: a + b, [1,[2,3],4], [5,[6,7],8]) + -> [6, [8, 10], 12] + """ + out = [] + for i in zip(*its): + if isinstance(i[0], Iterable) and not isinstance(i[0], str): + out.append(self._apply_func_its(func, *i)) + else: + out.append(func(*i)) + return out + + def __len__(self): + return len(self._data) diff --git a/core/sockets.py b/core/sockets.py index e4eb6e33c9..a833ecefe2 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -26,6 +26,7 @@ from sverchok.core.socket_data import ( SvGetSocketInfo, SvGetSocket, SvSetSocket, SvForgetSocket, SvNoDataError, sentinel) +from sverchok.core.socket_data_containers import ObjectSocketData from sverchok.data_structure import ( enum_item_4, @@ -253,6 +254,9 @@ def preprocess_input(self, data): return result def postprocess_output(self, data): + if not isinstance(data, (list, tuple)) \ + and any([self.use_flatten, self.use_simplify, self.use_graft, self.use_unwrap, self.use_wrap]): + raise NotImplementedError(f'Post processing is not supported for "{type(data).__name__}" class') result = data if self.use_flatten: result = self.do_flatten(data) @@ -396,7 +400,13 @@ def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): else: implicit_conversions = DEFAULT_CONVERSION - return self.convert_data(SvGetSocket(self, other, deepcopy), implicit_conversions, other) + data = self.convert_data(SvGetSocket(self, other, deepcopy), implicit_conversions, other) + + # in case if socket is using custom container + try: + return data.get_data() + except AttributeError: + return data prop_name = self.get_prop_name() if prop_name: @@ -600,6 +610,15 @@ def draw_property(self, layout, prop_origin=None, prop_name='default_property'): else: layout.prop_search(self, 'object_ref_pointer', bpy.data, 'objects', text=self.name) + def sv_set(self, data): + """ + This should solve problem of keeping references in valid state + The problem is that references to Blender data blocks can be outdated + The solution is refresh references if needed + For this reason some meta information should be stored in this method + """ + super().sv_set(ObjectSocketData(data)) + class SvFormulaSocket(NodeSocket, SvSocketCommon): bl_idname = "SvFormulaSocket" diff --git a/index.md b/index.md index 522652c5f1..e37df081d3 100644 --- a/index.md +++ b/index.md @@ -639,6 +639,7 @@ SvTimerNode --- SvDupliInstancesMK4 + SvBlenderDataListNode ## Objects SvVertexGroupNodeMK2 diff --git a/nodes/scene/blender_data_list.py b/nodes/scene/blender_data_list.py new file mode 100644 index 0000000000..feb89b3ab8 --- /dev/null +++ b/nodes/scene/blender_data_list.py @@ -0,0 +1,154 @@ +# 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 + +import bpy + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.utils.handle_blender_data import BPYPointers +from sverchok.utils.nodes_mixins.sv_animatable_nodes import SvAnimatableNode +from sverchok.data_structure import updateNode + + +class SvBlenderDataPointers(bpy.types.PropertyGroup): + + __annotations__ = dict() + for enum in BPYPointers: + __annotations__[enum.name.lower()] = bpy.props.PointerProperty( + type=enum.value, update=lambda s, c: updateNode(c.node, c)) + + +class SvEditDataBlockList(bpy.types.Operator): + bl_label = "Edit data block list" + bl_idname = "sverchok.edit_data_block_list" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + operations = ['add', 'remove', 'move_up', 'move_down', 'clear', 'get_selected', 'add_selected'] + operation: bpy.props.EnumProperty(items=[(i, i, '') for i in operations]) + + item_index: bpy.props.IntProperty() # required for some operations + + def execute(self, context): + node = context.node + + if self.operation == 'add': + node.blender_data.add() + elif self.operation == 'remove': + node.blender_data.remove(self.item_index) + elif self.operation == 'move_up': + next_index = max(0, self.item_index - 1) + node.blender_data.move(self.item_index, next_index) + elif self.operation == 'move_down': + next_index = min(self.item_index + 1, len(node.blender_data) - 1) + node.blender_data.move(self.item_index, next_index) + elif self.operation == 'clear': + node.blender_data.clear() + elif self.operation == 'get_selected': + node.blender_data.clear() + self.add_selected(context, node.blender_data) + elif self.operation == 'add_selected': + self.add_selected(context, node.blender_data) + + updateNode(node, context) + return {'FINISHED'} + + @staticmethod + def add_selected(context, collection): + # if any bugs then you have probably using depsgraph here + for obj in context.selected_objects: + item = collection.add() + item.object = obj + + +class SvDataBlockListOptions(bpy.types.Menu): + bl_idname = "OBJECT_MT_data_block_list_options" + bl_label = "Options" + + def draw(self, context): + layout = self.layout + node = context.node + + op = layout.operator(SvEditDataBlockList.bl_idname, text='Clear the list') + op.operation = 'clear' + + layout.separator() + layout.prop(node, 'is_animatable') + layout.prop(node, 'refresh', toggle=False, icon='FILE_REFRESH') + + +class UI_UL_SvBlenderDataList(bpy.types.UIList): + def draw_item(self, context, layout, node, item, icon, active_data, active_propname, index, flt_flag): + row = layout.row(align=True) + data_type = getattr(BPYPointers, node.data_type.upper()) + + row.prop_search(item, node.data_type, bpy.data, data_type.collection_name, text='') + + if node.edit_mode: + + up = row.operator(SvEditDataBlockList.bl_idname, text='', icon='TRIA_UP') + up.operation = 'move_up' + up.item_index = index + + down = row.operator(SvEditDataBlockList.bl_idname, text='', icon='TRIA_DOWN') + down.operation = 'move_down' + down.item_index = index + + remove = row.operator(SvEditDataBlockList.bl_idname, text='', icon='REMOVE') + remove.operation = 'remove' + remove.item_index = index + + def draw_filter(self, context, layout): + pass + + +class SvBlenderDataListNode(SvAnimatableNode, SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: list + Tooltip: + """ + bl_idname = 'SvBlenderDataListNode' + bl_label = 'Blender data list' + bl_icon = 'ALIGN_TOP' + + data_type: bpy.props.EnumProperty(items=[(e.name.lower(), e.name, '') for e in BPYPointers], name='Type', + update=updateNode) + edit_mode: bpy.props.BoolProperty(name='Edit list') + blender_data: bpy.props.CollectionProperty(type=SvBlenderDataPointers) + selected: bpy.props.IntProperty() + + def sv_init(self, context): + self.outputs.new('SvObjectSocket', 'Object') # todo change label? + self.blender_data.add() + + def draw_buttons(self, context, layout): + col = layout.column() + col.prop(self, 'data_type') + if self.data_type == 'object': + row = col.row(align=True) + row.label(text='Selected:') + row.operator(SvEditDataBlockList.bl_idname, text='Get').operation = 'get_selected' + row.operator(SvEditDataBlockList.bl_idname, text='Add').operation = 'add_selected' + row = col.row(align=True) + row.prop(self, 'edit_mode') + op = row.operator(SvEditDataBlockList.bl_idname, text='+') + op.operation = 'add' + row.menu(SvDataBlockListOptions.bl_idname, icon='DOWNARROW_HLT', text="") + layout.template_list(UI_UL_SvBlenderDataList.__name__, "blender_data", self, "blender_data", self, "selected") + + def process(self): + self.outputs['Object'].sv_set( + [getattr(p, self.data_type) for p in self.blender_data if getattr(p, self.data_type)]) + + +classes = [ + SvBlenderDataPointers, + SvEditDataBlockList, + SvDataBlockListOptions, + UI_UL_SvBlenderDataList, + SvBlenderDataListNode] + + +register, unregister = bpy.utils.register_classes_factory(classes) diff --git a/utils/handle_blender_data.py b/utils/handle_blender_data.py index e14a387c61..ed1db98dbe 100644 --- a/utils/handle_blender_data.py +++ b/utils/handle_blender_data.py @@ -317,6 +317,24 @@ def collection(self): } return collections[self] + @property + def collection_name(self) -> str: + """Returns name of collection of current pointer""" + names = { + BPYPointers.OBJECT: 'objects', + BPYPointers.MESH: 'meshes', + BPYPointers.NODE_TREE: 'node_groups', + BPYPointers.MATERIAL: 'materials', + BPYPointers.COLLECTION: 'collections', + BPYPointers.TEXT: 'texts', + BPYPointers.LIGHT: 'lights', + BPYPointers.IMAGE: 'images', + BPYPointers.TEXTURE: 'textures', + BPYPointers.VECTOR_FONT: 'curves', + BPYPointers.GREASE_PENCIL: 'grease_pencils' + } + return names[self] + @property def type(self): """Return Blender type of the pointer"""