diff --git a/core/sockets.py b/core/sockets.py index 80723931fd..bc1567da8f 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -44,7 +44,8 @@ "SvDummySocket": (0.8, 0.8, 0.8, 0.3), "SvSeparatorSocket": (0.0, 0.0, 0.0, 0.0), "SvObjectSocket": (0.69, 0.74, 0.73, 1.0), - "SvTextSocket": (0.68, 0.85, 0.90, 1) + "SvTextSocket": (0.68, 0.85, 0.90, 1), + "SvDictionarySocket": (1.0, 1.0, 1.0, 1.0) } def process_from_socket(self, context): @@ -59,6 +60,8 @@ class SvSocketCommon: use_expander: BoolProperty(default=True) use_quicklink: BoolProperty(default=True) expanded: BoolProperty(default=False) + custom_draw: StringProperty(description="For name of method which will draw socket UI (optionally)") + prop_name: StringProperty(default='', description="For displaying node property in socket UI") quicklink_func_name: StringProperty(default="", name="quicklink_func_name") @@ -269,7 +272,9 @@ class SvObjectSocket(NodeSocket, SvSocketCommon): object_ref: StringProperty(update=process_from_socket) def draw(self, context, layout, node, text): - if not self.is_output and not self.is_linked: + if self.custom_draw: + super().draw(context, layout, node, text) + elif not self.is_output and not self.is_linked: layout.prop_search(self, 'object_ref', bpy.data, 'objects', text=self.name) elif self.is_linked: layout.label(text=text + '. ' + SvGetSocketInfo(self)) @@ -320,7 +325,6 @@ class SvMatrixSocket(NodeSocket, SvSocketCommon): bl_idname = "SvMatrixSocket" bl_label = "Matrix Socket" - prop_name: StringProperty(default='') num_matrices: IntProperty(default=0) @property @@ -353,9 +357,7 @@ class SvVerticesSocket(NodeSocket, SvSocketCommon): bl_label ="Vertices Socket" prop: FloatVectorProperty(default=(0, 0, 0), size=3, update=process_from_socket) - prop_name: StringProperty(default='') use_prop: BoolProperty(default=False) - custom_draw: StringProperty() def get_prop_data(self): if self.prop_name: @@ -387,7 +389,6 @@ class SvQuaternionSocket(NodeSocket, SvSocketCommon): bl_label = "Quaternion Socket" prop: FloatVectorProperty(default=(1, 0, 0, 0), size=4, subtype='QUATERNION', update=process_from_socket) - prop_name: StringProperty(default='') use_prop: BoolProperty(default=False) def get_prop_data(self): @@ -420,7 +421,6 @@ class SvColorSocket(NodeSocket, SvSocketCommon): bl_label = "Color Socket" prop: FloatVectorProperty(default=(0, 0, 0, 1), size=4, subtype='COLOR', min=0, max=1, update=process_from_socket) - prop_name: StringProperty(default='') use_prop: BoolProperty(default=False) def get_prop_data(self): @@ -452,7 +452,6 @@ class SvDummySocket(NodeSocket, SvSocketCommon): bl_label = "Dummys Socket" prop: FloatVectorProperty(default=(0, 0, 0), size=3, update=process_from_socket) - prop_name: StringProperty(default='') use_prop: BoolProperty(default=False) def get_prop_data(self): @@ -470,8 +469,6 @@ class SvSeparatorSocket(NodeSocket, SvSocketCommon): bl_idname = "SvSeparatorSocket" bl_label = "Separator Socket" - prop_name: StringProperty(default='') - def draw(self, context, layout, node, text): # layout.label("") layout.label(text="——————") @@ -487,10 +484,8 @@ class SvStringsSocket(NodeSocket, SvSocketCommon): bl_idname = "SvStringsSocket" bl_label = "Strings Socket" - prop_name: StringProperty(default='') prop_type: StringProperty(default='') prop_index: IntProperty() - custom_draw: StringProperty() def get_prop_data(self): if self.prop_name: @@ -521,6 +516,72 @@ def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): else: raise SvNoDataError(self) + +class SvDictionarySocket(NodeSocket, SvSocketCommon): + '''For dictionary data''' + bl_idname = "SvDictionarySocket" + bl_label = "Dictionary Socket" + + def get_prop_data(self): + if self.prop_name: + return {"prop_name": self.prop_name} + else: + return {} + + def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): + if self.is_linked and not self.is_output: + source_data = SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) + return self.convert_data(source_data, implicit_conversions) + + if self.prop_name: + return [[getattr(self.node, self.prop_name)[:]]] + elif default is sentinel: + raise SvNoDataError(self) + else: + return default + + +class SvChameleonSocket(NodeSocket, SvSocketCommon): + '''Using as input socket with color of other connected socket''' + bl_idname = "SvChameleonSocket" + bl_label = "Chameleon Socket" + + dynamic_color: FloatVectorProperty(default=(0.0, 0.0, 0.0, 0.0), size=4, + description="For storing color of other socket via catch_props method") + dynamic_type: StringProperty(default='SvChameleonSocket', + description="For storing type of other socket via catch_props method") + + def catch_props(self): + # should be called during update event of a node for catching its property + other = self.other + if other: + self.dynamic_color = socket_colors[other.bl_idname] + self.dynamic_type = other.bl_idname + else: + self.dynamic_color = (0.0, 0.0, 0.0, 0.0) + self.dynamic_type = self.bl_idname + + def get_prop_data(self): + if self.prop_name: + return {"prop_name": self.prop_name} + else: + return {} + + def sv_get(self, default=sentinel, deepcopy=True): + if self.is_linked and not self.is_output: + return SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) + + if self.prop_name: + return [[getattr(self.node, self.prop_name)[:]]] + elif default is sentinel: + raise SvNoDataError(self) + else: + return default + + def draw_color(self, context, node): + return self.dynamic_color + + """ type_map_to/from are used to get the bl_idname from a single letter @@ -550,7 +611,7 @@ def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): classes = [ SvVerticesSocket, SvMatrixSocket, SvStringsSocket, SvColorSocket, SvQuaternionSocket, SvDummySocket, SvSeparatorSocket, - SvTextSocket, SvObjectSocket + SvTextSocket, SvObjectSocket, SvDictionarySocket, SvChameleonSocket ] register, unregister = bpy.utils.register_classes_factory(classes) diff --git a/docs/nodes/dictionary/dictionary_in.rst b/docs/nodes/dictionary/dictionary_in.rst new file mode 100644 index 0000000000..b3aafba3cd --- /dev/null +++ b/docs/nodes/dictionary/dictionary_in.rst @@ -0,0 +1,41 @@ +Dictionary in +============= + +.. image:: https://user-images.githubusercontent.com/28003269/71763474-bd89a080-2ef5-11ea-9b5d-e4526c1e5357.png + +Functionality +------------- + +The node creates dictionary with costume keys and given data. +It can be used for preparing data structure which is required for some nodes. + +Category +-------- + +Dictionary -> dictionary in + +Inputs +------ + +- **-----** - can be connected with any other output socket of any type (10 maximum connections) + +Outputs +------- + +- **Dict** - dictionary(ies) + + +Examples +-------- + +**Creating complex data structure:** + +.. image:: https://user-images.githubusercontent.com/28003269/71763703-51f50280-2ef8-11ea-845a-f924cd53f79d.png + +**Does not use the same keys:** + +.. image:: https://user-images.githubusercontent.com/28003269/71763737-be700180-2ef8-11ea-8cf9-4f8cf94286cb.png + +**Does not try join dictionaries with different keys, only keys of first dictionary in a list are taken in account:** + +.. image:: https://user-images.githubusercontent.com/28003269/71763756-fe36e900-2ef8-11ea-8ae0-300aebc95ad1.png \ No newline at end of file diff --git a/docs/nodes/dictionary/dictionary_index.rst b/docs/nodes/dictionary/dictionary_index.rst new file mode 100644 index 0000000000..c7200e3875 --- /dev/null +++ b/docs/nodes/dictionary/dictionary_index.rst @@ -0,0 +1,9 @@ +********** +Dictionary +********** + +.. toctree:: + :maxdepth: 2 + + dictionary_in + dictionary_out \ No newline at end of file diff --git a/docs/nodes/dictionary/dictionary_out.rst b/docs/nodes/dictionary/dictionary_out.rst new file mode 100644 index 0000000000..fa176c979f --- /dev/null +++ b/docs/nodes/dictionary/dictionary_out.rst @@ -0,0 +1,29 @@ +Dictionary out +============== + +.. image:: https://user-images.githubusercontent.com/28003269/71804968-6a8f2500-307e-11ea-85ce-8c3c7619ee0a.png + +Functionality +------------- + +The node unpacks dictionary and assign values of each key to sockets with names of keys. + +Category +-------- + +Dictionary -> dictionary out + +Inputs +------ + +- **Dict** - dictionary(ies) + +Outputs +------- + +According keys of input dictionary. If multiple dictionary are given only keys of first dictionary are taken in account. + +Examples +-------- + +.. image:: https://user-images.githubusercontent.com/28003269/71805215-218ba080-307f-11ea-8321-1886a62837c5.png \ No newline at end of file diff --git a/index.md b/index.md index b4fb5067d8..9eb2c783d8 100644 --- a/index.md +++ b/index.md @@ -191,6 +191,10 @@ ListShuffleNode ListSortNodeMK2 ListFlipNode + +## Dictionary + SvDictionaryIn + SvDictionaryOut ## CAD SvBevelNode diff --git a/nodes/dictionary/__init__.py b/nodes/dictionary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nodes/dictionary/dictionary_in.py b/nodes/dictionary/dictionary_in.py new file mode 100644 index 0000000000..a5f3b5e1ec --- /dev/null +++ b/nodes/dictionary/dictionary_in.py @@ -0,0 +1,165 @@ +# 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 itertools import chain, cycle + +import bpy + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode + + +class SvDict(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + """ + Special attribute for keeping meta data which helps to unwrap dictionaries properly for `dictionary out` node + This attribute should be set only by nodes which create new dictionaries or change existing one + Order of keys in `self.inputs` dictionary should be the same as order of input data of the dictionary + `self.inputs` dictionary should keep data in next format: + {data.id: # it helps to track data, if is changed `dictionary out` recreate new socket for this data + {'type': any socket type with which input data is related, + 'name': name of output socket, + 'nest': only for 'SvDictionarySocket' type, should keep dictionary with the same data structure + }} + + For example, there is the dictionary: + dict('My values': [0,1,2], 'My vertices': [(0,0,0), (1,0,0), (0,1,0)]) + + Metadata should look in this way: + self.inputs = {'Values id': + {'type': 'SvStringsSocket', + {'name': 'My values', + {'nest': None + } + 'Vertices id': + {'type': 'SvVerticesSocket', + {'name': 'My vertices', + {'nest': None + } + } + """ + self.inputs = dict() + + +class SvDictionaryIn(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Put given data to dictionary with custom key + + Each key should be unique + Can have nested dictionaries + """ + bl_idname = 'SvDictionaryIn' + bl_label = 'Dictionary in' + bl_icon = 'GREASEPENCIL' + + def update_node(self, context): + if not self['update_event']: + updateNode(self, context) + + def lift_item(self, context): + if self.up: + sock_ind = list(self.inputs).index(context.socket) + if sock_ind > 0: + self.inputs.move(sock_ind, sock_ind - 1) + self.up = False + + def down_item(self, context): + if self.down: + sock_ind = list(self.inputs).index(context.socket) + if sock_ind < len(self.inputs) - 2: + self.inputs.move(sock_ind, sock_ind + 1) + self.down = False + + keys = set(f"key_{i}" for i in range(10)) + + up: bpy.props.BoolProperty(update=lift_item) + down: bpy.props.BoolProperty(update=down_item) + alert: bpy.props.BoolVectorProperty(size=10) + key_0: bpy.props.StringProperty(name="", default='Key 1', update=update_node) + key_1: bpy.props.StringProperty(name="", default='Key 2', update=update_node) + key_2: bpy.props.StringProperty(name="", default='Key 3', update=update_node) + key_3: bpy.props.StringProperty(name="", default='Key 4', update=update_node) + key_4: bpy.props.StringProperty(name="", default='Key 5', update=update_node) + key_5: bpy.props.StringProperty(name="", default='Key 6', update=update_node) + key_6: bpy.props.StringProperty(name="", default='Key 7', update=update_node) + key_7: bpy.props.StringProperty(name="", default='Key 8', update=update_node) + key_8: bpy.props.StringProperty(name="", default='Key 9', update=update_node) + key_9: bpy.props.StringProperty(name="", default='Key 10', update=update_node) + + def sv_init(self, context): + self.inputs.new('SvChameleonSocket', 'Data') + self.outputs.new('SvDictionarySocket', 'Dict') + self['update_event'] = False # if True the node does not update upon properties changes + + def update(self): + # Remove unused sockets + [self.inputs.remove(sock) for sock in list(self.inputs)[:-1] if not sock.is_linked] + [sock.catch_props() for sock in self.inputs if sock.links] + + # add property to new socket and add extra empty socket + if list(self.inputs)[-1].is_linked and len(self.inputs) < 11: + free_keys = self.keys - set(sock.prop_name for sock in list(self.inputs)[:-1]) + last_sock = list(self.inputs)[-1] + last_sock.prop_name = free_keys.pop() + last_sock.custom_draw = 'draw_socket' + self.inputs.new('SvChameleonSocket', 'Data') + self['update_event'] = True + setattr(self, last_sock.prop_name, last_sock.other.name) + self['update_event'] = False + + def draw_socket(self, socket, context, layout): + layout.prop(self, 'up', text='', icon='TRIA_UP') + layout.prop(self, 'down', text='', icon='TRIA_DOWN') + layout.alert = self.alert[int(socket.prop_name.rsplit('_', 1)[-1])] + layout.prop(self, socket.prop_name) + + def validate_names(self): + # light string properties with equal keys + used_names = set() + invalid_names = set() + for sock in list(self.inputs)[:-1]: + name = getattr(self, sock.prop_name) + if name not in used_names: + used_names.add(name) + else: + invalid_names.add(name) + for sock in list(self.inputs)[:-1]: + if getattr(self, sock.prop_name) in invalid_names: + self.alert[int(sock.prop_name.rsplit('_', 1)[-1])] = True + else: + self.alert[int(sock.prop_name.rsplit('_', 1)[-1])] = False + + def process(self): + + if not any((sock.links for sock in self.inputs)): + return + + self.validate_names() + + max_len = max([len(sock.sv_get()) for sock in list(self.inputs)[:-1]]) + data = [chain(sock.sv_get(), cycle([None])) for sock in list(self.inputs)[:-1]] + keys = [sock.prop_name for sock in list(self.inputs)[:-1]] + out = [] + for i, *props in zip(range(max_len), *data): + out_dict = SvDict({getattr(self, key): prop for key, prop in zip(keys, props) if prop is not None}) + for sock in list(self.inputs)[:-1]: + out_dict.inputs[sock.identifier] = { + 'type': sock.dynamic_type, + 'name': getattr(self, sock.prop_name), + 'nest': sock.sv_get()[0].inputs if sock.dynamic_type == 'SvDictionarySocket' else None} + out.append(out_dict) + self.outputs[0].sv_set(out) + + +def register(): + [bpy.utils.register_class(cl) for cl in [SvDictionaryIn]] + + +def unregister(): + [bpy.utils.unregister_class(cl) for cl in [SvDictionaryIn][::-1]] diff --git a/nodes/dictionary/dictionary_out.py b/nodes/dictionary/dictionary_out.py new file mode 100644 index 0000000000..1530e72252 --- /dev/null +++ b/nodes/dictionary/dictionary_out.py @@ -0,0 +1,128 @@ +# 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 itertools import chain, cycle + +import bpy +from mathutils import Matrix, Quaternion + +from sverchok.node_tree import SverchCustomTreeNode + + +def get_socket_type(data, sub_cls=None, size=None): + """ + If input dictionary does not have metadata + this function is used for determining type of output socket according given data type + :param data: any data + :param sub_cls: always None, for internal usage only + :param size: always None, for internal usage only + :return: returns one of Sverchok socket type, string + """ + types = {bpy.types.Object: 'SvObjectSocket', + Matrix: 'SvMatrixSocket', + Quaternion: 'SvQuaternionSocket', + float: ('SvStringsSocket', 'SvVerticesSocket', 'SvColorSocket'), + int: 'SvStringsSocket'} + + if type(data) is dict: + return 'SvDictionarySocket' + if hasattr(data, '__iter__'): + return get_socket_type(data[0], type(data), len(data)) + elif type(data) in types: + socket_type = types[type(data)] + if socket_type == ('SvStringsSocket', 'SvVerticesSocket', 'SvColorSocket'): + if sub_cls is tuple: + return 'SvVerticesSocket' if size == 3 else 'SvColorSocket' + else: + return 'SvStringsSocket' + else: + return socket_type + else: + raise TypeError(f"Which type of socket this type ({type(data)}) of data should be?") + + +class SvDictionaryOut(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Unwrap given dictionary + + For each key of dictionary new socket is created + Keys of first given dictionary in a list are taken in account + """ + bl_idname = 'SvDictionaryOut' + bl_label = 'Dictionary out' + bl_icon = 'OUTLINER_DATA_GP_LAYER' + + def sv_init(self, context): + self.inputs.new('SvDictionarySocket', 'Dict') + self['order'] = [] # keeps keys of given dictionary since last update event + + def update(self): + if not self.inputs['Dict'].links: # if link is unconnected from the socket, `is_linked` is steal True + self.outputs.clear() + self['order'] = [] + + def rebuild_output(self): + # draw output sockets according given keys of input dictionary + # dictionary with metadata kept in `inputs` attribute can be handled + # this can be called during node update event and from process of the node (after update event) + out_dict = self.inputs['Dict'].sv_get()[0] + if hasattr(out_dict, 'inputs'): + # handle dictionary with metadata + if self['order'] != list(out_dict.inputs.keys()): + # order is changed, sockets should be rebuild + with self.sv_throttle_tree_update(): + links = {sock.name: [link.to_socket for link in sock.links] for sock in self.outputs} + self.outputs.clear() + new_order = [] + new_socks = [] + for key, data in out_dict.inputs.items(): + sock = self.outputs.new(data['type'], data['name']) + new_order.append(key) + new_socks.append(sock) + self['order'] = new_order + [self.id_data.links.new(sock, other_socket) for sock in new_socks if sock.name in links + for other_socket in links[sock.name]] + else: + # order is unchanged but renaming of sockets should be done anywhere + # in case keys of a dictionary was changed + for sock, sock_id in zip(self.outputs, self['order']): + sock.name = out_dict.inputs[sock_id]['name'] + else: + # handle dictionary without metadata + if self['order'] != list(self.inputs['Dict'].sv_get()[0].keys()): + with self.sv_throttle_tree_update(): + links = {sock.name: [link.to_socket for link in sock.links] for sock in self.outputs} + self.outputs.clear() + new_socks = [self.outputs.new(get_socket_type(data), key) for key, data in + self.inputs['Dict'].sv_get()[0].items()] + self['order'] = list(self.inputs['Dict'].sv_get()[0].keys()) + [self.id_data.links.new(sock, other_socket) for sock in new_socks if sock.name in links + for other_socket in links[sock.name]] + + def process(self): + + if not self.inputs['Dict'].links: + return + + self.rebuild_output() + + out = {key: [] for key in self.inputs['Dict'].sv_get()[0]} + for d in self.inputs['Dict'].sv_get(): + for key in d: + if key in out: + out[key].append(d[key]) + + [self.outputs[key].sv_set(out[key]) for key in out if key in self.outputs] + + +def register(): + [bpy.utils.register_class(cl) for cl in [SvDictionaryOut]] + + +def unregister(): + [bpy.utils.unregister_class(cl) for cl in [SvDictionaryOut][::-1]] diff --git a/ui/nodeview_space_menu.py b/ui/nodeview_space_menu.py index 2c72ed332f..0ff72d0dfb 100644 --- a/ui/nodeview_space_menu.py +++ b/ui/nodeview_space_menu.py @@ -120,6 +120,7 @@ def draw(self, context): layout.menu("NODEVIEW_MT_AddQuaternion", **icon('SV_QUATERNION')) layout.menu("NODEVIEW_MT_AddLogic", **icon("SV_LOGIC")) layout.menu("NODEVIEW_MT_AddListOps", **icon('NLA')) + layout.menu("NODEVIEW_MT_AddDictionary", icon='OUTLINER_OB_FONT') layout.separator() layout.menu("NODEVIEW_MT_AddViz", **icon('RESTRICT_VIEW_OFF')) layout.menu("NODEVIEW_MT_AddText", icon='TEXT') @@ -211,6 +212,7 @@ def draw(self, context): make_class('Layout', "Layout"), make_class('Listmain', "List Main"), make_class('Liststruct', "List Struct"), + make_class('Dictionary', "Dictionary"), make_class('Number', "Number"), make_class('Vector', "Vector"), make_class('Matrix', "Matrix"),