Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data block list #3903

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down
109 changes: 109 additions & 0 deletions core/socket_data_containers.py
Original file line number Diff line number Diff line change
@@ -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)
21 changes: 20 additions & 1 deletion core/sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions index.md
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@
SvTimerNode
---
SvDupliInstancesMK4
SvBlenderDataListNode

## Objects
SvVertexGroupNodeMK2
Expand Down
154 changes: 154 additions & 0 deletions nodes/scene/blender_data_list.py
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 18 additions & 0 deletions utils/handle_blender_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down