From d6aa48602d8cc06e3017e1b4c7fc12e0ac4be9dd Mon Sep 17 00:00:00 2001 From: Durman Date: Thu, 2 Sep 2021 08:33:28 +0400 Subject: [PATCH 01/25] switch from SvGetSocketInfo to socket.objects_number core.sockets_data is only for internal usage core.socket module --- nodes/color/color_out_mk1.py | 11 ++++------- nodes/color/texture_evaluate_mk2.py | 4 ++-- nodes/text/debug_print.py | 3 +-- nodes/transforms/texture_displace_mk2.py | 3 +-- nodes/viz/viewer_2d.py | 3 +-- nodes/viz/viewer_draw_mk4.py | 9 +++------ old_nodes/texture_displace.py | 4 ++-- old_nodes/texture_evaluate.py | 4 ++-- 8 files changed, 16 insertions(+), 25 deletions(-) diff --git a/nodes/color/color_out_mk1.py b/nodes/color/color_out_mk1.py index 13178c9867..347f4af685 100644 --- a/nodes/color/color_out_mk1.py +++ b/nodes/color/color_out_mk1.py @@ -16,15 +16,11 @@ # # ##### END GPL LICENSE BLOCK ##### -import colorsys import bpy -from bpy.props import FloatProperty, BoolProperty, FloatVectorProperty -from mathutils import Color +from bpy.props import BoolProperty, FloatVectorProperty from sverchok.node_tree import SverchCustomTreeNode -from sverchok.core.socket_data import SvGetSocketInfo -from sverchok.data_structure import updateNode, dataCorrect, dataCorrect_np -from sverchok.utils.sv_itertools import sv_zip_longest +from sverchok.data_structure import updateNode, dataCorrect_np from sverchok.utils.modules.color_utils import rgb_to_hsv, rgb_to_hsl from numpy import ndarray, array @@ -92,7 +88,8 @@ def draw_color_socket(self, socket, context, layout): else: - layout.label(text=socket.name+ '. ' + SvGetSocketInfo(socket)) + layout.label(text=socket.name+ '. ' + str(socket.objects_number)) + def draw_buttons(self, context, layout): layout.prop(self, 'selected_mode', expand=True) # layout.prop(self, 'use_alpha') diff --git a/nodes/color/texture_evaluate_mk2.py b/nodes/color/texture_evaluate_mk2.py index 72a70ce57a..1a88e30912 100644 --- a/nodes/color/texture_evaluate_mk2.py +++ b/nodes/color/texture_evaluate_mk2.py @@ -22,7 +22,6 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.utils.nodes_mixins.sv_animatable_nodes import SvAnimatableNode -from sverchok.core.socket_data import SvGetSocketInfo from sverchok.utils.sv_IO_pointer_helpers import unpack_pointer_property_name from sverchok.data_structure import (updateNode, list_match_func, numpy_list_match_modes, iter_list_match_func, no_space) @@ -143,7 +142,8 @@ def draw_texture_socket(self, socket, context, layout): c.prop_search(self, "texture_pointer", bpy.data, 'textures', text="") else: - layout.label(text=socket.name+ '. ' + SvGetSocketInfo(socket)) + layout.label(text=socket.name+ '. ' + str(socket.objects_number)) + def draw_buttons(self, context, layout): self.draw_animatable_buttons(layout, icon_only=True) b = layout.split(factor=0.33, align=True) diff --git a/nodes/text/debug_print.py b/nodes/text/debug_print.py index 27d3e6a0bf..d7dfb448f4 100644 --- a/nodes/text/debug_print.py +++ b/nodes/text/debug_print.py @@ -21,7 +21,6 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import multi_socket, updateNode -from sverchok.core.socket_data import SvGetSocketInfo defaults = [True for i in range(32)] @@ -54,7 +53,7 @@ def draw_buttons_ext(self, context, layout): layout.prop(self, "print_socket", index=i, text=socket.name) def draw_socket_boolean(self, socket, context, layout): - text = f"{socket.name}. {SvGetSocketInfo(socket)}" + text = f"{socket.name}. {str(socket.objects_number)}" layout.label(text=text) icon = ("HIDE_ON", "HIDE_OFF", )[self.print_socket[socket.index]] layout.prop(self, "print_socket", icon=icon, index=socket.index, text="") diff --git a/nodes/transforms/texture_displace_mk2.py b/nodes/transforms/texture_displace_mk2.py index 7708d2c864..37c9897251 100644 --- a/nodes/transforms/texture_displace_mk2.py +++ b/nodes/transforms/texture_displace_mk2.py @@ -22,7 +22,6 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.utils.nodes_mixins.sv_animatable_nodes import SvAnimatableNode -from sverchok.core.socket_data import SvGetSocketInfo from sverchok.data_structure import updateNode, list_match_func, numpy_list_match_modes from sverchok.utils.sv_IO_pointer_helpers import unpack_pointer_property_name from sverchok.utils.sv_itertools import recurse_f_level_control @@ -164,7 +163,7 @@ def draw_texture_socket(self, socket, context, layout): c.label(text=socket.name+ ':') c.prop_search(self, "texture_pointer", bpy.data, 'textures', text="") else: - layout.label(text=socket.name+ '. ' + SvGetSocketInfo(socket)) + layout.label(text=socket.name+ '. ' + str(socket.objects_number)) def draw_buttons(self, context, layout): is_vector = self.out_mode in ['RGB to XYZ', 'HSV to XYZ', 'HLS to XYZ'] diff --git a/nodes/viz/viewer_2d.py b/nodes/viz/viewer_2d.py index 5066bcb33f..f6e4a740e9 100644 --- a/nodes/viz/viewer_2d.py +++ b/nodes/viz/viewer_2d.py @@ -28,7 +28,6 @@ import gpu from gpu_extras.batch import batch_for_shader -from sverchok.core.socket_data import SvGetSocketInfo from sverchok.data_structure import updateNode, node_id from sverchok.node_tree import SverchCustomTreeNode from sverchok.ui import bgl_callback_nodeview as nvBGL @@ -711,7 +710,7 @@ def draw_color_socket(self, socket, context, layout): layout.prop(self, socket.prop_name, text="") else: if draw_name: - layout.label(text=socket.name+ '. ' + SvGetSocketInfo(socket)) + layout.label(text=socket.name+ '. ' + str(socket.objects_number)) def get_drawing_attributes(self): """ obtain the dpi adjusted xy and scale factors, cache location_theta """ diff --git a/nodes/viz/viewer_draw_mk4.py b/nodes/viz/viewer_draw_mk4.py index e67c0f4303..221ab27b06 100644 --- a/nodes/viz/viewer_draw_mk4.py +++ b/nodes/viz/viewer_draw_mk4.py @@ -9,7 +9,7 @@ from itertools import cycle from mathutils import Vector, Matrix -from mathutils.geometry import tessellate_polygon as tessellate, normal +from mathutils.geometry import tessellate_polygon as tessellate from mathutils.noise import random, seed_set import bpy from bpy.props import StringProperty, FloatProperty, IntProperty, EnumProperty, BoolProperty, FloatVectorProperty @@ -17,10 +17,7 @@ import gpu from gpu_extras.batch import batch_for_shader -import sverchok -from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata from sverchok.utils.sv_mesh_utils import polygons_to_edges_np -from sverchok.core.socket_data import SvGetSocketInfo from sverchok.data_structure import updateNode, node_id, match_long_repeat, enum_item_5 from sverchok.node_tree import SverchCustomTreeNode from sverchok.ui.bgl_callback_3dview import callback_disable, callback_enable @@ -747,7 +744,7 @@ def migrate_from(self, old_node): def draw_property_socket(self, socket, context, layout): drawing_verts = socket.name == "Vertices" prop_to_show = "point_size" if drawing_verts else "line_width" - text = f"{socket.name}. {SvGetSocketInfo(socket)}" + text = f"{socket.name}. {str(socket.objects_number)}" layout.label(text=text) layout.prop(self, prop_to_show, text="px") @@ -774,7 +771,7 @@ def draw_color_socket(self, socket, context, layout): else: if draw_name: reduced_name = socket.name[:2] + ". Col" - layout.label(text=reduced_name+ '. ' + SvGetSocketInfo(socket)) + layout.label(text=reduced_name+ '. ' + str(socket.objects_number)) def create_config(self): config = lambda: None diff --git a/old_nodes/texture_displace.py b/old_nodes/texture_displace.py index 475c754718..de83bd255e 100644 --- a/old_nodes/texture_displace.py +++ b/old_nodes/texture_displace.py @@ -22,7 +22,6 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.utils.nodes_mixins.sv_animatable_nodes import SvAnimatableNode -from sverchok.core.socket_data import SvGetSocketInfo from sverchok.data_structure import updateNode, list_match_func, numpy_list_match_modes from sverchok.utils.sv_itertools import recurse_f_level_control from sverchok.utils.modules.color_utils import color_channels @@ -173,7 +172,8 @@ def draw_texture_socket(self, socket, context, layout): c.label(text=socket.name+ ':') c.prop_search(self, "name_texture", bpy.data, 'textures', text="") else: - layout.label(text=socket.name+ '. ' + SvGetSocketInfo(socket)) + layout.label(text=socket.name+ '. ' + str(socket.objects_number)) + def draw_buttons(self, context, layout): is_vector = self.out_mode in ['RGB to XYZ', 'HSV to XYZ', 'HLS to XYZ'] self.draw_animatable_buttons(layout, icon_only=True) diff --git a/old_nodes/texture_evaluate.py b/old_nodes/texture_evaluate.py index 155961cbff..d370a04acb 100644 --- a/old_nodes/texture_evaluate.py +++ b/old_nodes/texture_evaluate.py @@ -22,7 +22,6 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.utils.nodes_mixins.sv_animatable_nodes import SvAnimatableNode -from sverchok.core.socket_data import SvGetSocketInfo from sverchok.data_structure import (updateNode, list_match_func, numpy_list_match_modes, iter_list_match_func, no_space) from sverchok.utils.sv_itertools import recurse_f_level_control @@ -144,7 +143,8 @@ def draw_texture_socket(self, socket, context, layout): c.prop_search(self, "name_texture", bpy.data, 'textures', text="") else: - layout.label(text=socket.name+ '. ' + SvGetSocketInfo(socket)) + layout.label(text=socket.name+ '. ' + str(socket.objects_number)) + def draw_buttons(self, context, layout): self.draw_animatable_buttons(layout, icon_only=True) b = layout.split(factor=0.33, align=True) From 73d0762e6f85038fc6f207fcf02da10b45b10db8 Mon Sep 17 00:00:00 2001 From: Durman Date: Thu, 2 Sep 2021 13:18:33 +0400 Subject: [PATCH 02/25] Move functionality of getting data of neighbour node to update system this should improve performance significantly during animation and large node trees Some nodes like loops, groups do not work or do this not fully correct for now --- core/__init__.py | 7 +- core/group_handlers.py | 7 +- core/handlers.py | 4 +- core/main_tree_handler.py | 33 ++++++- core/socket_data.py | 122 +++++++------------------- core/sockets.py | 64 +++++--------- core/update_system.py | 5 -- utils/nodes_mixins/recursive_nodes.py | 3 +- 8 files changed, 100 insertions(+), 145 deletions(-) diff --git a/core/__init__.py b/core/__init__.py index 62c4333d4f..0ec299d942 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,7 +1,6 @@ import importlib import sverchok -from sverchok.utils.logging import debug, exception -from sverchok.core.update_system import clear_system_cache +from sverchok.core.socket_data import clear_all_socket_cache reload_event = False @@ -12,7 +11,7 @@ core_modules = [ "sv_custom_exceptions", - "sockets", + "sockets", "socket_data", "handlers", "update_system", "main_tree_handler", "events", "node_group", "group_handlers" ] @@ -25,7 +24,7 @@ def sv_register_modules(modules): m.register() def sv_unregister_modules(modules): - clear_system_cache() + clear_all_socket_cache() for m in reversed(modules): if hasattr(m, "unregister"): try: diff --git a/core/group_handlers.py b/core/group_handlers.py index d6cdb01f49..34db332277 100644 --- a/core/group_handlers.py +++ b/core/group_handlers.py @@ -17,7 +17,7 @@ from typing import Generator, Dict, TYPE_CHECKING, Union, List, NamedTuple, Optional, Iterator, NewType, Tuple from sverchok.core.events import GroupEvent -from sverchok.core.main_tree_handler import empty_updater, NodesUpdater, CancelError, ContextTrees +from sverchok.core.main_tree_handler import empty_updater, NodesUpdater, CancelError, ContextTrees, get_sock_data from sverchok.utils.tree_structure import Tree, Node from sverchok.utils.logging import log_error from sverchok.utils.handle_blender_data import BlNode @@ -355,6 +355,8 @@ def group_node_updater(node: Node, group_nodes_path=None) -> Generator[Node, Non previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) should_be_updated = (not node.is_updated or node.is_input_changed or previous_nodes_are_changed) yield node # yield groups node so it be colored by node Updater if necessary + if should_be_updated: + get_sock_data(node) updater = node.bl_tween.updater(group_nodes_path=group_nodes_path, is_input_changed=should_be_updated) is_output_changed, out_error = yield from updater node.is_input_changed = False @@ -372,9 +374,12 @@ def node_updater(node: Node, group_node: SvGroupTreeNode): node_error = None try: if bl_node.bl_idname in {'NodeGroupInput', 'NodeGroupOutput'}: + if bl_node.bl_idname == 'NodeGroupOutput': + get_sock_data(node) bl_node.process(group_node) elif hasattr(bl_node, 'process'): yield node # yield only normal nodes + get_sock_data(node) bl_node.process() node.is_updated = True except CancelError as e: diff --git a/core/handlers.py b/core/handlers.py index 6fda9739cc..a58581d052 100644 --- a/core/handlers.py +++ b/core/handlers.py @@ -3,7 +3,7 @@ from sverchok import old_nodes from sverchok import data_structure -from sverchok.core.update_system import clear_system_cache, reset_timing_graphs +from sverchok.core.socket_data import clear_all_socket_cache from sverchok.ui import bgl_callback_nodeview, bgl_callback_3dview from sverchok.utils import app_handler_ops from sverchok.utils.handle_blender_data import BlTrees @@ -149,7 +149,7 @@ def sv_pre_load(scene): 3. post_load handler 4. evaluate trees from main tree handler """ - clear_system_cache() + clear_all_socket_cache() sv_clean(scene) import sverchok.core.group_handlers as gh diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index a07d4a9bd2..001659061b 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -12,6 +12,8 @@ from typing import Dict, NamedTuple, Generator, Optional, Iterator, Tuple, Union import bpy +from sverchok.core.socket_data import SvNoDataError +from sverchok.core.socket_conversions import ConversionPolicies from sverchok.data_structure import post_load_call from sverchok.core.events import TreeEvent, GroupEvent from sverchok.utils.logging import debug, catch_log_error, log_error @@ -37,7 +39,7 @@ def send(event: TreeEvent): # can arrive before timer finishes its tusk. Or timer can start working before frame change is handled. if event.type == TreeEvent.FRAME_CHANGE: ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - list(global_updater(event.type)) + profile(section="UPDATE")(lambda: list(global_updater(event.type)))() return # mark given nodes as outdated @@ -451,6 +453,7 @@ def node_updater(node: Node, *args) -> Generator[Node, None, Optional[Exception] if should_be_updated: try: yield node + get_sock_data(node) node.bl_tween.process(*args) node.is_updated = True node.is_output_changed = True @@ -469,6 +472,8 @@ def group_node_updater(node: Node) -> Generator[Node, None, Tuple[bool, Optional previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) should_be_updated = (not node.is_updated or node.is_input_changed or previous_nodes_are_changed) yield node # yield groups node so it be colored by node Updater if necessary + if should_be_updated: + get_sock_data(node) updater = node.bl_tween.updater(is_input_changed=should_be_updated) is_output_changed, out_error = yield from updater node.is_input_changed = False @@ -490,6 +495,32 @@ def empty_updater(node: Node = None, **kwargs): yield +def get_sock_data(node: Node): + """Get data from previous nodes. Should be called before given node execution""" + for in_sock in node.inputs: + for out_sock in in_sock.linked_sockets: + + # reroute nodes should be treated separately, they does not have socket catch + # and data should be searched in previous nodes + # it would be nice to standardize their API but its seems impossible without node.copy trigger + if out_sock.node.bl_tween.bl_idname == 'NodeReroute': + while True: + prev_socks = out_sock.node.inputs[0].linked_sockets + if prev_socks: + out_sock = prev_socks[0] + if out_sock.node.bl_tween.bl_idname != 'NodeReroute': + break + else: + raise SvNoDataError(in_sock.bl_tween) + + # get data from normal node + data = out_sock.bl_tween.sv_get() + if out_sock.bl_tween.bl_idname != in_sock.bl_tween.bl_idname: + implicit_conversions = ConversionPolicies.get_conversion(in_sock.bl_tween.default_conversion_name) + data = implicit_conversions.convert(in_sock.bl_tween, out_sock.bl_tween, data) + in_sock.bl_tween.sv_set(data) + + @post_load_call def post_load_register(): # when new file is loaded all timers are unregistered diff --git a/core/socket_data.py b/core/socket_data.py index 087a755658..769049ee7a 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -16,26 +16,23 @@ # # ##### END GPL LICENSE BLOCK ##### -from sverchok import data_structure -from sverchok.utils.logging import warning, info, debug +"""For internal usage of the sockets module""" -##################################### -# socket data cache # -##################################### +from collections import defaultdict +from typing import Dict, NewType, Union, Optional -sentinel = object() - -# socket cache -socket_data_cache = {} - -# faster than builtin deep copy for us. -# useful for our limited case -# we should be able to specify vectors here to get them create -# or stop destroying them when in vector socket. +SockAddress = NewType('SockAddress', str) +SockContext = NewType('SockContext', str) # socket can have multiple values in case it used inside node group +DataAddress = Dict[SockAddress, Dict[Union[SockContext, None], Optional[list]]] +socket_data_cache: DataAddress = defaultdict(lambda: defaultdict(lambda: None)) def sv_deep_copy(lst): """return deep copied data of list/tuple structure""" + # faster than builtin deep copy for us. + # useful for our limited case + # we should be able to specify vectors here to get them create + # or stop destroying them when in vector socket. if isinstance(lst, (list, tuple)): if lst and not isinstance(lst[0], (list, tuple)): return lst[:] @@ -43,79 +40,36 @@ def sv_deep_copy(lst): return lst -# Build string for showing in socket label -def SvGetSocketInfo(socket): - """returns string to show in socket label""" - global socket_data_cache - ng = socket.id_data.tree_id - - if socket.is_output: - s_id = socket.socket_id - elif socket.is_linked: - other = socket.other - if other and hasattr(other, 'socket_id'): - s_id = other.socket_id - else: - return '' - else: - return '' - if ng in socket_data_cache: - if s_id in socket_data_cache[ng]: - data = socket_data_cache[ng][s_id] - if data: - return str(len(data)) - return '' - -def SvForgetSocket(socket): +def sv_forget_socket(socket): """deletes socket data from cache""" - global socket_data_cache - if data_structure.DEBUG_MODE: - if not socket.is_output: - warning(f"{socket.node.name} forgetting input socket: {socket.name}") - s_id = socket.socket_id - s_ng = socket.id_data.tree_id try: - socket_data_cache[s_ng].pop(s_id, None) + del socket_data_cache[_get_sock_address(socket)] except KeyError: - debug("it was never there") + pass -def SvSetSocket(socket, out): - """sets socket data for socket""" - global socket_data_cache - s_id = socket.socket_id - s_ng = socket.id_data.tree_id - try: - socket_data_cache[s_ng][s_id] = out - except KeyError: - socket_data_cache[s_ng] = {} - socket_data_cache[s_ng][s_id] = out +def sv_set_socket(socket, data, context: SockContext = None): + """sets socket data for socket""" + socket_data_cache[_get_sock_address(socket)][context] = data -def SvGetSocket(socket, other=None, deepcopy=True): +def sv_get_socket(socket, deepcopy=True, context: SockContext = None): """gets socket data from socket, if deep copy is True a deep copy is make_dep_dict, to increase performance if the node doesn't mutate input set to False and increase performance substanstilly """ - global socket_data_cache - try: - s_id = other.socket_id - s_ng = other.id_data.tree_id - out = socket_data_cache[s_ng][s_id] - if deepcopy: - return sv_deep_copy(out) - return out - - except Exception as e: - if data_structure.DEBUG_MODE: - if socket.node is not None or other.node is not None: - debug(f"cache miss: {socket.node.name} -> {socket.name} from: {other.node.name} -> {other.name}") - else: - debug(f"Cache miss. A socket was recently created, it is not bound with a node yet") + data = socket_data_cache[_get_sock_address(socket)][context] + if data is not None: + return sv_deep_copy(data) if deepcopy else data + else: raise SvNoDataError(socket) +def _get_sock_address(sock) -> SockAddress: + return sock.id_data.tree_id + sock.socket_id + + class SvNoDataError(LookupError): def __init__(self, socket=None, node=None, msg=None): @@ -148,35 +102,23 @@ def __unicode__(self): def __format__(self, spec): return repr(self) -def get_output_socket_data(node, output_socket_name): + +def get_output_socket_data(node, output_socket_name, context: SockContext = None): """ This method is intended to usage in internal tests mainly. Get data that the node has written to the output socket. Raises SvNoDataError if it hasn't written any. """ - - global socket_data_cache - - tree_name = node.id_data.tree_id - socket = node.outputs[output_socket_name] - socket_id = socket.socket_id - if tree_name not in socket_data_cache: - raise SvNoDataError() - if socket_id in socket_data_cache[tree_name]: - return socket_data_cache[tree_name][socket_id] + socket = node.inputs[output_socket_name] # todo why output? + sock_address = _get_sock_address(socket) + if sock_address in socket_data_cache and context in socket_data_cache[sock_address]: + return socket_data_cache[sock_address][context] else: raise SvNoDataError(socket) -def reset_socket_cache(ng): - """ - Reset socket cache either for node group. - """ - global socket_data_cache - socket_data_cache[ng.tree_id] = {} def clear_all_socket_cache(): """ Reset socket cache for all node-trees. """ - global socket_data_cache socket_data_cache.clear() diff --git a/core/sockets.py b/core/sockets.py index 7f3dd59b7b..9c481078f7 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -24,8 +24,8 @@ from sverchok.core.socket_conversions import ConversionPolicies from sverchok.core.socket_data import ( - SvGetSocketInfo, SvGetSocket, SvSetSocket, SvForgetSocket, - SvNoDataError, sentinel) + sv_get_socket, sv_set_socket, sv_forget_socket, + SvNoDataError) from sverchok.data_structure import ( enum_item_4, @@ -51,8 +51,6 @@ import Part STANDARD_TYPES = STANDARD_TYPES + (Part.Shape,) -DEFAULT_CONVERSION = ConversionPolicies.DEFAULT.conversion - def process_from_socket(self, context): """Update function of exposed properties in Sockets""" @@ -307,6 +305,7 @@ class SvSocketCommon(SvSocketProcessing): """ color = (1, 0, 0, 1) # base color, other sockets should override the property, use FloatProperty for dynamic + default_conversion_name = ConversionPolicies.DEFAULT.conversion_name label: StringProperty() # It will be drawn instead of name if given quick_link_to_node = str() # sockets which often used with other nodes can fill its `bl_idname` here link_menu_handler : StringProperty(default='') # To specify additional entries in the socket link menu @@ -382,33 +381,27 @@ def hide_safe(self, value): self.hide = value - def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): + def sv_get(self, default=..., deepcopy=True, context=None): """ - The method is used for getting input socket data + The method is used for getting socket data In most cases the method should not be overridden - If socket uses custom implicit_conversion it should implements default_conversion_name attribute Also a socket can use its default_property Order of getting data (if available): - 1. written socket data + 1. written socket data (for output sockets this is the only option) 2. node default property 3. socket default property 4. script default property 5. Raise no data error :param default: script default property :param deepcopy: in most cases should be False for efficiency but not in cases if input data will be modified - :param implicit_conversions: if needed automatic conversion data from one socket type to another + :param context: provide this in case the node can be evaluated several times in different contexts :return: data bound to the socket """ + if self.is_output: + return sv_get_socket(self, False, context) - if self.is_linked and not self.is_output: - other = self.other - if implicit_conversions is None: - if hasattr(self, 'default_conversion_name'): - implicit_conversions = ConversionPolicies.get_conversion(self.default_conversion_name) - else: - implicit_conversions = DEFAULT_CONVERSION - - return self.convert_data(SvGetSocket(self, other, deepcopy), implicit_conversions, other) + if self.is_linked: + return sv_get_socket(self, deepcopy, context) prop_name = self.get_prop_name() if prop_name: @@ -419,19 +412,20 @@ def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): default_property = self.default_property return format_bpy_property(default_property) - if default is not sentinel: + if default is not ...: return default raise SvNoDataError(self) - def sv_set(self, data): - """Set output data""" - data = self.postprocess_output(data) - SvSetSocket(self, data) + def sv_set(self, data, context: str = None): + """Set data, provide context in case the node can be evaluated several times in different context""" + if self.is_output: + data = self.postprocess_output(data) + sv_set_socket(self, data, context=context) def sv_forget(self): """Delete socket memory""" - SvForgetSocket(self) + sv_forget_socket(self) def replace_socket(self, new_type, new_name=None): """Replace a socket with a socket of new_type and keep links, @@ -536,31 +530,20 @@ def draw_label(text): def draw_color(self, context, node): return self.color - def convert_data(self, source_data, implicit_conversions=DEFAULT_CONVERSION, other=None): - - if other.bl_idname == self.bl_idname: - return source_data - - return implicit_conversions.convert(self, other, source_data) - - def update_objects_number(self): + def update_objects_number(self, context=None): # todo should be context here? """ Should be called each time after process method of the socket owner It will update number of objects to show in socket labels """ try: - if self.is_output: - objects_info = SvGetSocketInfo(self) - self.objects_number = int(objects_info) if objects_info else 0 - else: - data = self.sv_get(deepcopy=False, default=[]) - self.objects_number = len(data) if data else 0 + self.objects_number = len(self.sv_get(deepcopy=False, default=[], context=context)) except LookupError: - pass + self.objects_number = 0 except Exception as e: warning(f"Socket='{self.name}' of node='{self.node.name}' can't update number of objects on the label. " f"Cause is '{e}'") self.objects_number = 0 + raise e class SvObjectSocket(NodeSocket, SvSocketCommon): @@ -639,10 +622,11 @@ def update_depth(self, context): default_conversion_name = ConversionPolicies.LENIENT.conversion_name def draw(self, context, layout, node, text): - layout.label(text=self.name+ '. ' + SvGetSocketInfo(self)) + layout.label(text=self.name+ '. ' + str(self.objects_number)) layout.prop(self,'depth',text='Depth') layout.prop(self,'transform',text='') + class SvTextSocket(NodeSocket, SvSocketCommon): bl_idname = "SvTextSocket" bl_label = "Text Socket" diff --git a/core/update_system.py b/core/update_system.py index f0c2ebe25a..47b0ca7979 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -38,11 +38,6 @@ exception_color = (0.8, 0.0, 0) -def clear_system_cache(): - print("cleaning Sverchok cache") - clear_all_socket_cache() - - def update_error_colors(self, context): global no_data_color global exception_color diff --git a/utils/nodes_mixins/recursive_nodes.py b/utils/nodes_mixins/recursive_nodes.py index 5bb38156b9..e30590e9e8 100644 --- a/utils/nodes_mixins/recursive_nodes.py +++ b/utils/nodes_mixins/recursive_nodes.py @@ -8,14 +8,13 @@ from mathutils import Matrix from bpy.props import BoolProperty, IntVectorProperty from sverchok.utils.sv_itertools import process_matched -from sverchok.core.socket_data import sentinel from sverchok.data_structure import (updateNode, list_match_func, numpy_list_match_modes, ensure_nesting_level, ensure_min_nesting) from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata DEFAULT_TYPES = { - 'NONE': sentinel, + 'NONE': ..., 'EMPTY_LIST': [[]], 'MATRIX': [Matrix()], 'MASK': [[True]] From 49ac0af176528ec7735dd821288c40ab2de421f7 Mon Sep 17 00:00:00 2001 From: Durman Date: Tue, 7 Sep 2021 16:49:09 +0400 Subject: [PATCH 03/25] Move SvNoDataError into sv_custom_exceptions module --- core/main_tree_handler.py | 2 +- core/sockets.py | 5 ++-- core/sv_custom_exceptions.py | 36 +++++++++++++++++++++++++++++ core/update_system.py | 2 +- node_tree.py | 2 +- nodes/curve/bezier_spline.py | 2 +- nodes/curve/offset_mk2.py | 2 +- nodes/curve/offset_on_surface.py | 2 +- nodes/script/profile_mk3.py | 2 +- nodes/spatial/concave_hull.py | 1 - nodes/spatial/field_random_probe.py | 2 +- nodes/spatial/populate_solid.py | 2 +- nodes/spatial/populate_surface.py | 2 +- old_nodes/field_random_probe_mk2.py | 2 +- old_nodes/populate_solid.py | 2 +- old_nodes/populate_surface.py | 2 +- utils/testing.py | 4 ++-- 17 files changed, 53 insertions(+), 19 deletions(-) diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index 001659061b..bfad82a2ab 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -12,7 +12,7 @@ from typing import Dict, NamedTuple, Generator, Optional, Iterator, Tuple, Union import bpy -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError, CancelError from sverchok.core.socket_conversions import ConversionPolicies from sverchok.data_structure import post_load_call from sverchok.core.events import TreeEvent, GroupEvent diff --git a/core/sockets.py b/core/sockets.py index 9c481078f7..9cf3851fbf 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -23,9 +23,8 @@ from bpy.types import NodeTree, NodeSocket from sverchok.core.socket_conversions import ConversionPolicies -from sverchok.core.socket_data import ( - sv_get_socket, sv_set_socket, sv_forget_socket, - SvNoDataError) +from sverchok.core.socket_data import sv_get_socket, sv_set_socket, sv_forget_socket +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.data_structure import ( enum_item_4, diff --git a/core/sv_custom_exceptions.py b/core/sv_custom_exceptions.py index ccad652269..942f1d1cdb 100644 --- a/core/sv_custom_exceptions.py +++ b/core/sv_custom_exceptions.py @@ -6,6 +6,42 @@ # License-Filename: LICENSE +class SvNoDataError(LookupError): + def __init__(self, socket=None, node=None, msg=None): + + self.extra_message = msg if msg else "" + + if node is None and socket is not None: + node = socket.node + self.node = node + self.socket = socket + + super(LookupError, self).__init__(self.get_message()) + + def get_message(self): + if self.extra_message: + return f"node {self.socket.node.name} (socket {self.socket.name}) {self.extra_message}" + if not self.node and not self.socket: + return "SvNoDataError" + else: + return f"No data passed into socket '{self.socket.name}'" + + def __repr__(self): + return self.get_message() + + def __str__(self): + return repr(self) + + def __unicode__(self): + return repr(self) + + def __format__(self, spec): + return repr(self) + + +class CancelError(Exception): + """Aborting tree evaluation by user""" + class SvProcessingError(Exception): pass diff --git a/core/update_system.py b/core/update_system.py index 47b0ca7979..d2383f9f70 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -23,7 +23,7 @@ import bpy from sverchok import data_structure -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.utils.logging import warning, error, exception from sverchok.utils.profile import profile from sverchok.core.socket_data import clear_all_socket_cache diff --git a/node_tree.py b/node_tree.py index 08c0fd05f3..c9451d4578 100644 --- a/node_tree.py +++ b/node_tree.py @@ -13,7 +13,7 @@ from bpy.props import StringProperty, BoolProperty, EnumProperty from bpy.types import NodeTree -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.core.events import TreeEvent from sverchok.core.main_tree_handler import TreeHandler from sverchok.core.group_handlers import NodeIdManager diff --git a/nodes/curve/bezier_spline.py b/nodes/curve/bezier_spline.py index e872a6d6a8..af7e8e544e 100644 --- a/nodes/curve/bezier_spline.py +++ b/nodes/curve/bezier_spline.py @@ -4,7 +4,7 @@ import bpy from bpy.props import EnumProperty, BoolProperty -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level from sverchok.utils.curve import SvBezierCurve, SvCubicBezierCurve diff --git a/nodes/curve/offset_mk2.py b/nodes/curve/offset_mk2.py index 9eaf17a3ea..9b9cfcb592 100644 --- a/nodes/curve/offset_mk2.py +++ b/nodes/curve/offset_mk2.py @@ -4,7 +4,7 @@ import bpy from bpy.props import FloatProperty, EnumProperty, IntProperty -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level from sverchok.utils.curve import SvCurve, SvOffsetCurve diff --git a/nodes/curve/offset_on_surface.py b/nodes/curve/offset_on_surface.py index 3f485b0f10..39028641fa 100644 --- a/nodes/curve/offset_on_surface.py +++ b/nodes/curve/offset_on_surface.py @@ -1,7 +1,7 @@ import bpy from bpy.props import FloatProperty, EnumProperty, IntProperty -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level from sverchok.utils.curve import SvCurve, SvCurveOffsetOnSurface diff --git a/nodes/script/profile_mk3.py b/nodes/script/profile_mk3.py index 8581faf973..d6cf8a07f9 100644 --- a/nodes/script/profile_mk3.py +++ b/nodes/script/profile_mk3.py @@ -26,7 +26,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.utils.nodes_mixins.sv_animatable_nodes import SvAnimatableNode from sverchok.utils.sv_node_utils import sync_pointer_and_stored_name -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.data_structure import updateNode, match_long_repeat from sverchok.utils.logging import info, debug, warning from sverchok.utils.curve.algorithms import concatenate_curves, unify_curves_degree diff --git a/nodes/spatial/concave_hull.py b/nodes/spatial/concave_hull.py index ab1815f993..d41b19023d 100644 --- a/nodes/spatial/concave_hull.py +++ b/nodes/spatial/concave_hull.py @@ -8,7 +8,6 @@ import bpy from bpy.props import FloatProperty, StringProperty, BoolProperty, EnumProperty, IntProperty -from sverchok.core.socket_data import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, ensure_nesting_level, zip_long_repeat, get_data_nesting_level from sverchok.utils.alpha_shape import alpha_shape diff --git a/nodes/spatial/field_random_probe.py b/nodes/spatial/field_random_probe.py index d83c7e5ae9..474958e121 100644 --- a/nodes/spatial/field_random_probe.py +++ b/nodes/spatial/field_random_probe.py @@ -13,7 +13,7 @@ from sverchok.core.sockets import setup_new_node_location from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.utils.field.scalar import SvScalarField from sverchok.utils.field.probe import field_random_probe diff --git a/nodes/spatial/populate_solid.py b/nodes/spatial/populate_solid.py index f117bbd211..c567a0fabf 100644 --- a/nodes/spatial/populate_solid.py +++ b/nodes/spatial/populate_solid.py @@ -10,7 +10,7 @@ import bpy from bpy.props import FloatProperty, BoolProperty, EnumProperty, IntProperty -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, ensure_nesting_level, zip_long_repeat, repeat_last_for_length,\ get_data_nesting_level diff --git a/nodes/spatial/populate_surface.py b/nodes/spatial/populate_surface.py index 26389460f2..80a7916688 100644 --- a/nodes/spatial/populate_surface.py +++ b/nodes/spatial/populate_surface.py @@ -8,7 +8,7 @@ import bpy from bpy.props import EnumProperty, IntProperty, BoolProperty, FloatProperty -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level from sverchok.utils.surface import SvSurface diff --git a/old_nodes/field_random_probe_mk2.py b/old_nodes/field_random_probe_mk2.py index 98fd7567ae..1e944ac1d6 100644 --- a/old_nodes/field_random_probe_mk2.py +++ b/old_nodes/field_random_probe_mk2.py @@ -13,7 +13,7 @@ from sverchok.core.sockets import setup_new_node_location from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.utils.field.scalar import SvScalarField from sverchok.utils.field.probe import field_random_probe diff --git a/old_nodes/populate_solid.py b/old_nodes/populate_solid.py index 6c6c0ae5d7..373a776cd4 100644 --- a/old_nodes/populate_solid.py +++ b/old_nodes/populate_solid.py @@ -10,7 +10,7 @@ import bpy from bpy.props import FloatProperty, BoolProperty, EnumProperty, IntProperty -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, ensure_nesting_level, zip_long_repeat, repeat_last_for_length from sverchok.utils.field.scalar import SvScalarField diff --git a/old_nodes/populate_surface.py b/old_nodes/populate_surface.py index a0577ae570..f75344c635 100644 --- a/old_nodes/populate_surface.py +++ b/old_nodes/populate_surface.py @@ -8,7 +8,7 @@ import bpy from bpy.props import IntProperty, BoolProperty, FloatProperty -from sverchok.core.socket_data import SvNoDataError +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level from sverchok.utils.surface import SvSurface diff --git a/utils/testing.py b/utils/testing.py index 714bb2b120..df763b1d3a 100644 --- a/utils/testing.py +++ b/utils/testing.py @@ -14,9 +14,9 @@ import sverchok from sverchok import old_nodes -from sverchok.old_nodes import is_old from sverchok.data_structure import get_data_nesting_level -from sverchok.core.socket_data import SvNoDataError, get_output_socket_data +from sverchok.core.socket_data import get_output_socket_data +from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.utils.logging import debug, info, exception from sverchok.utils.context_managers import sv_preferences from sverchok.utils.modules_inspection import iter_submodule_names From 62ddfadf95a67079a701ce2fcab62fe85aa1a6db Mon Sep 17 00:00:00 2001 From: Durman Date: Thu, 9 Sep 2021 14:13:40 +0400 Subject: [PATCH 04/25] Big refactoring of update system Update system does not use changing node IDs anymore Now update system delivers data from output sockets to input including inside group nodes Chunk of complexity was moved to Python tree data structure The approach never the less does not seem final There are node properties which are used too extensively but performance improvement for another time Reroute nodes are not included into Python tree model now --- core/group_handlers.py | 392 +++++++++++--------------------------- core/handlers.py | 7 - core/main_tree_handler.py | 305 +++++++++++++++-------------- core/node_group.py | 26 +-- core/socket_data.py | 59 ++---- core/sockets.py | 19 +- core/update_system.py | 1 - node_tree.py | 20 +- utils/tree_structure.py | 227 ++++++++++++++++++++-- 9 files changed, 525 insertions(+), 531 deletions(-) diff --git a/core/group_handlers.py b/core/group_handlers.py index 34db332277..9c1733cded 100644 --- a/core/group_handlers.py +++ b/core/group_handlers.py @@ -12,13 +12,13 @@ from __future__ import annotations -from collections import defaultdict from time import time -from typing import Generator, Dict, TYPE_CHECKING, Union, List, NamedTuple, Optional, Iterator, NewType, Tuple +from typing import Generator, TYPE_CHECKING, Union, List, Optional, Iterator, Tuple from sverchok.core.events import GroupEvent -from sverchok.core.main_tree_handler import empty_updater, NodesUpdater, CancelError, ContextTrees, get_sock_data -from sverchok.utils.tree_structure import Tree, Node +from sverchok.core.main_tree_handler import empty_updater, NodesUpdater, ContextTrees, handle_node_data, PathManager +from sverchok.core.sv_custom_exceptions import CancelError +from sverchok.utils.tree_structure import Node from sverchok.utils.logging import log_error from sverchok.utils.handle_blender_data import BlNode @@ -28,40 +28,35 @@ SvTree = Union[SvGroupTree, SverchCustomTree] SvNode = Union[SverchCustomTreeNode, SvGroupTreeNode] -NodeId = NewType('NodeId', str) -Path = NewType('Path', str) - class MainHandler: @classmethod - def update(cls, event: GroupEvent) -> Iterator[Node]: + def update(cls, event: GroupEvent, trees_ui_to_update: set) -> Iterator[Node]: """ This method should be called by group nodes for updating their tree Also it means that input data was changed """ - path = NodeIdManager.generate_path(event.group_nodes_path) - [NodesStatuses.mark_outdated(n, path) for n in event.updated_nodes] - return group_tree_handler(event.group_nodes_path) + ContextTrees.mark_nodes_outdated( + event.tree, event.updated_nodes, PathManager.generate_path(event.group_nodes_path)) + return group_tree_handler(event.group_nodes_path, trees_ui_to_update) @classmethod def send(cls, event: GroupEvent): - # just replace nodes IDs and return (should be first, does not call cancel or add task) todo should it be here? - if event.type == GroupEvent.EDIT_GROUP_NODE: - path = NodeIdManager.generate_path(event.group_nodes_path) - NodeIdManager.replace_nodes_id(event.tree, path) - return # there is no need in updating anything - # this should be first other wise other instructions can spoil the node statistic to redraw if NodesUpdater.is_running(): NodesUpdater.cancel_task() # mark given nodes as outdated if event.type == GroupEvent.NODES_UPDATE: - [NodesStatuses.mark_outdated(n) for n in event.updated_nodes] + ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) # it will find (before the tree evaluation) changes in tree topology and mark related nodes as outdated elif event.type == GroupEvent.GROUP_TREE_UPDATE: - GroupContextTrees.mark_tree_outdated(event.tree) + ContextTrees.mark_tree_outdated(event.tree) + + # trigger just to evaluate debug nodes and update tree ui + elif event.type == GroupEvent.EDIT_GROUP_NODE: + ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) elif event.type == GroupEvent.GROUP_NODE_UPDATE: raise TypeError(f'"Group node update" event should use update method instead of send') @@ -77,274 +72,83 @@ def send(cls, event: GroupEvent): @staticmethod def get_error_nodes(group_nodes_path: List[SvGroupTreeNode]) -> Iterator[Optional[Exception]]: """Returns error if a node has error during execution or None""" - path = NodeIdManager.generate_path(group_nodes_path) + path = PathManager.generate_path(group_nodes_path) + tree = ContextTrees.get(group_nodes_path[-1].node_tree, rebuild=False) for node in group_nodes_path[-1].node_tree.nodes: - yield NodesStatuses.get(node, path).error + if node.bl_idname in {'NodeReroute', 'NodeFrame'}: + yield None + continue + with tree.set_exec_context(path): + error = tree.nodes[node.name].error + yield error @staticmethod def get_nodes_update_time(group_nodes_path: List[SvGroupTreeNode]) -> Iterator[Optional[float]]: """Returns duration of a node being executed in milliseconds or None if there was an error""" - path = NodeIdManager.generate_path(group_nodes_path) + path = PathManager.generate_path(group_nodes_path) + tree = ContextTrees.get(group_nodes_path[-1].node_tree, rebuild=False) for node in group_nodes_path[-1].node_tree.nodes: - yield NodesStatuses.get(node, path).update_time + if node.bl_idname in {'NodeReroute', 'NodeFrame'}: + yield None + continue + with tree.set_exec_context(path): + upd_time = tree.nodes[node.name].update_time + yield upd_time @staticmethod def get_cum_time(group_nodes_path: List[SvGroupTreeNode]) -> Iterator[Optional[float]]: - path = NodeIdManager.generate_path(group_nodes_path) bl_tree = group_nodes_path[-1].node_tree - cum_time_nodes = GroupContextTrees.calc_cam_update_time(bl_tree, path) - for node in bl_tree.nodes: + cum_time_nodes = ContextTrees.calc_cam_update_time_group(bl_tree, group_nodes_path) + for node in group_nodes_path[-1].node_tree.nodes: yield cum_time_nodes.get(node) -# it is now inconsistent with the main tree handler module because is_updates can't be removed from here right now -# it is used by the NodeStatuses class to keep update status of nodes -class NodeStatistic(NamedTuple): - """ - Statistic should be kept separately for each node - because each node can have 10 or even 100 of different statistic profiles according number of group nodes using it - """ - is_updated: bool = False - error: Exception = None - update_time: float = None # sec - - -class NodesStatuses: - """ - It keeps node attributes which can be sensitive to context evaluation (path) - """ - _statuses: Dict[NodeId, Union[NodeStatistic, Dict[Path, NodeStatistic]]] = defaultdict(dict) - - @classmethod - def mark_outdated(cls, bl_node: SvNode, path: Optional[Path] = None): - """ - Try find given nodes in statistic and if find mark them as outdated - if path is not given it will mark as outdated for all node contexts - """ - node_id = NodeIdManager.extract_node_id(bl_node) - if node_id in cls._statuses: - if isinstance(cls._statuses[node_id], dict): - if path is not None: - if path in cls._statuses[node_id]: - del cls._statuses[node_id][path] - else: - del cls._statuses[node_id] - else: - del cls._statuses[node_id] - - @classmethod - def get(cls, bl_node: SvNode, path: Path) -> NodeStatistic: - # saved tree can't be used here because it can contain outdated nodes (especially node.index attribute) - # so called tree should be recreated, it should be done because node_id is dependent on tree topology - node_id = NodeIdManager.extract_node_id(bl_node) - if isinstance(cls._statuses[node_id], NodeStatistic): - return cls._statuses[node_id] - elif path in cls._statuses[node_id]: - return cls._statuses[node_id][path] - else: - return NodeStatistic() - - @classmethod - def set(cls, bl_node: SvNode, path: Path, stat: NodeStatistic): - """ - path should be empty ("") for all nodes which are not connected to input group nodes - it will protect useless node recalculation (such nodes should be calculated only once) - """ - node_id = NodeIdManager.extract_node_id(bl_node) - empty_path = Path('') - if path == empty_path: - cls._statuses[node_id] = stat - else: - if not isinstance(cls._statuses[node_id], dict): - cls._statuses[node_id] = {path: stat} - else: - cls._statuses[node_id][path] = stat - - @classmethod - def reset_data(cls): - """This method should be called before opening new file to free all statistic data""" - cls._statuses.clear() - - -class NodeIdManager: - """Responsible for handling node_ids, should be deleted in future refactorings""" - @classmethod - def replace_nodes_id(cls, tree: Union[SvGroupTree, Tree], path: Path = ''): - """ - The idea is to replace nodes ID before evaluating the tree - in this case sockets will get unique identifiers relative to base group node - - format of new nodes ID -> "group_node_id.node_id" ("group_node_id." is replaceable part unlike "node_id") - but nodes which is not connected with input should not change their ID - because the result of their process method will be constant between different group nodes - - group_node_id also can consist several paths -> "base_group_id.current_group_id" - in case when the group is inside another group - max length of path should be no more then number of base trees of most nested group node + 1 - """ - if hasattr(tree, 'bl_idname'): # it's Blender tree - tree = Tree(tree) - - # todo should be cashed for optimization? - input_linked_nodes = {n for n in tree.bfs_walk([tree.nodes.active_input] if tree.nodes.active_output else [])} - - for node in tree.nodes: - node_id = cls.extract_node_id(node.bl_tween) - - if not BlNode(node.bl_tween).is_debug_node and node in input_linked_nodes: - node.bl_tween.n_id = path + '.' + node_id - else: - node.bl_tween.n_id = node_id - - @classmethod - def generate_path(cls, group_nodes: List[SvGroupTreeNode]) -> Path: - return Path('.'.join(cls.extract_node_id(n) for n in group_nodes)) - - @staticmethod - def extract_node_id(bl_node: SvNode) -> NodeId: - *previous_group_node_id, node_id = bl_node.node_id.rsplit('.', 1) - return node_id - - -class GroupContextTrees(ContextTrees): - """ - The same tree but nodes has statistic dependently on context evaluation - For example node can has is_updated=True for tree evaluated from one group node and False for another - For using this class nodes of blender tree should have proper node_ids - """ - _trees: Dict[str, Tree] = dict() - - @classmethod - def get(cls, bl_tree: SvTree, path: Path): - """Return caught tree with filled `is_updated` attribute according last statistic""" - tree = cls._trees.get(bl_tree.tree_id) - - # new tree, all nodes are outdated - if tree is None: - tree = Tree(bl_tree) - cls._trees[bl_tree.tree_id] = tree - - # topology of the tree was changed and should be updated - elif not tree.is_updated: - tree = cls._update_tree(bl_tree) - cls._trees[bl_tree.tree_id] = tree - - # we have to always update is_updated status because the tree does not keep them properly - for node in tree.nodes: - node.is_updated = NodesStatuses.get(node.bl_tween, path).is_updated # fill in actual is_updated state - - return tree - - @classmethod - def calc_cam_update_time(cls, bl_tree, path: Path) -> dict: - cum_time_nodes = dict() - if bl_tree.tree_id not in cls._trees: - return cum_time_nodes - - tree = cls._trees[bl_tree.tree_id] - out_nodes = [n for n in tree.nodes if BlNode(n.bl_tween).is_debug_node] - out_nodes.extend([tree.nodes.active_output] if tree.nodes.active_output else []) - for node in tree.sorted_walk(out_nodes): - update_time = NodesStatuses.get(node.bl_tween, path).update_time - if update_time is None: # error node? - cum_time_nodes[node.bl_tween] = None - continue - if len(node.last_nodes) > 1: - cum_time = sum(NodesStatuses.get(n.bl_tween, path).update_time for n in tree.sorted_walk([node]) - if NodesStatuses.get(n.bl_tween, path).update_time is not None) - else: - cum_time = sum(cum_time_nodes.get(n.bl_tween, 0) for n in node.last_nodes) + update_time - cum_time_nodes[node.bl_tween] = cum_time - return cum_time_nodes - - @classmethod - def _update_tree(cls, bl_tree: SvTree): - """ - This method will generate new tree and update 'is_input_changed' node attribute - according topological changes relatively previous call - """ - new_tree = Tree(bl_tree) - - # update is_input_changed attribute - cls._update_topology_status(new_tree) - - return new_tree - - @classmethod - def mark_nodes_outdated(cls, bl_tree, bl_nodes): - raise RuntimeError("Use the NodeStatuses classes instead") - - -def group_tree_handler(group_nodes_path: List[SvGroupTreeNode])\ +def group_tree_handler(group_nodes_path: List[SvGroupTreeNode], trees_ui_to_update: set)\ -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: - # The function is growing bigger and bigger. I wish I knew how to simplify it. group_node = group_nodes_path[-1] - path = NodeIdManager.generate_path(group_nodes_path) - tree = GroupContextTrees.get(group_node.node_tree, path) - NodeIdManager.replace_nodes_id(tree, path) + path = PathManager.generate_path(group_nodes_path) + tree = ContextTrees.get(group_node.node_tree, path) + is_debug_to_update = group_node.node_tree in trees_ui_to_update \ + and group_node.node_tree.group_node_name == group_node.name out_nodes = [n for n in tree.nodes if BlNode(n.bl_tween).is_debug_node] out_nodes.extend([tree.nodes.active_output] if tree.nodes.active_output else []) - input_linked_nodes = {n for n in tree.bfs_walk([tree.nodes.active_input] if tree.nodes.active_output else [])} - output_linked_nodes = {n for n in tree.bfs_walk(out_nodes, direction='DOWNWARD')} - # output output_was_changed = False node_error = None - for node in tree.sorted_walk(out_nodes): - if BlNode(node.bl_tween).is_debug_node: - continue # debug nodes will be updated after all by NodesUpdater only if necessary - can_be_updated = all(n.is_updated for n in node.last_nodes) - should_be_updated = can_be_updated and ((not node.is_updated) or node.is_input_changed) + with tree.set_exec_context(path): + for node in tree.sorted_walk(out_nodes): + can_be_updated = all(n.is_updated for n in node.last_nodes) + if not can_be_updated: + # here different logic can be implemented but for this we have to know if is there any output of the node + # we could leave the node as updated and don't broke work of the rest forward nodes + # but if the node does not have any output all next nodes will gen NoDataError what is horrible + node.is_updated = False + node.is_output_changed = False + continue - # reset current statistic - if should_be_updated: - node.is_updated = False - else: - continue - - # update node with sub update system - if hasattr(node.bl_tween, 'updater'): - sub_updater = group_node_updater(node, group_nodes_path) - # regular nodes - elif hasattr(node.bl_tween, 'process'): - sub_updater = node_updater(node, group_node) - # reroutes - else: - node.is_updated = True - sub_updater = empty_updater(it_output_changed=True, node_error=None) - - start_time = time() - is_output_changed, node_error = yield from sub_updater - update_time = time() - start_time - - # update current node statistic if there was any updates - node_path = Path('') if node not in input_linked_nodes else path - stat = NodeStatistic(node.is_updated, node_error, update_time if not node_error else None) - NodesStatuses.set(node.bl_tween, node_path, stat) - - # if update was successful - if is_output_changed: - - # reset next nodes statistics (only for context nodes connected to global nodes) - if node not in input_linked_nodes: - for next_node in node.next_nodes: - if next_node in input_linked_nodes: - # this should cause arising all next node statistic because input was changed by global node - NodesStatuses.set(next_node.bl_tween, Path(''), NodeStatistic(False)) - - # next nodes should be update too then - for next_node in node.next_nodes: - next_node.is_updated = False - # statistic of below nodes should be set directly into NodesStatuses - # because they won't be updated with current task - if next_node not in output_linked_nodes: - NodesStatuses.set(next_node.bl_tween, Path(''), NodeStatistic(False)) - - # output of group tree was changed - if node.bl_tween.bl_idname == 'NodeGroupOutput': + # update node with sub update system + if hasattr(node.bl_tween, 'updater'): + sub_updater = group_node_updater(node, group_nodes_path) + # regular nodes + elif hasattr(node.bl_tween, 'process'): + sub_updater = node_updater(node, group_node, is_debug_to_update) + # reroutes + else: + node.is_updated = True + sub_updater = empty_updater(it_output_changed=True, node_error=None) + + start_time = time() + node_error = yield from sub_updater + update_time = time() - start_time + + if node.is_output_changed or node_error: + node.error = node_error + node.update_time = None if node_error else update_time + + if node.is_output_changed and node.bl_tween.bl_idname == 'NodeGroupOutput': output_was_changed = True return output_was_changed, node_error @@ -355,36 +159,54 @@ def group_node_updater(node: Node, group_nodes_path=None) -> Generator[Node, Non previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) should_be_updated = (not node.is_updated or node.is_input_changed or previous_nodes_are_changed) yield node # yield groups node so it be colored by node Updater if necessary - if should_be_updated: - get_sock_data(node) updater = node.bl_tween.updater(group_nodes_path=group_nodes_path, is_input_changed=should_be_updated) - is_output_changed, out_error = yield from updater + with handle_node_data(node): + is_output_changed, out_error = yield from updater node.is_input_changed = False node.is_updated = not out_error node.is_output_changed = is_output_changed - return is_output_changed, out_error + return out_error -def node_updater(node: Node, group_node: SvGroupTreeNode): +def node_updater(node: Node, group_node: SvGroupTreeNode, is_debug_to_update: bool): """ Group tree should have proper node_ids before calling this method Also this method will mark next nodes as outdated for current context """ - bl_node = node.bl_tween + if BlNode(node.bl_tween).is_debug_node and not is_debug_to_update: + return None, None # Early exit otherwise it will spoil node statuses + node_error = None - try: - if bl_node.bl_idname in {'NodeGroupInput', 'NodeGroupOutput'}: - if bl_node.bl_idname == 'NodeGroupOutput': - get_sock_data(node) - bl_node.process(group_node) - elif hasattr(bl_node, 'process'): - yield node # yield only normal nodes - get_sock_data(node) - bl_node.process() - node.is_updated = True - except CancelError as e: - node_error = e - except Exception as e: - node_error = e - log_error(e) - return not node_error, node_error + + previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) + should_be_updated = not node.is_updated or node.is_input_changed or previous_nodes_are_changed + + node.is_output_changed = False # it should always False unless the process method was called + node.is_input_changed = False # if node wont be able to handle new input it will be seen in its update status + if should_be_updated: + try: + with handle_node_data(node): + if node.bl_tween.bl_idname in {'NodeGroupInput', 'NodeGroupOutput'}: + node.bl_tween.process(group_node) + else: + yield node # yield only normal nodes + node.bl_tween.process() + node.is_updated = True + node.is_output_changed = True + + # is_output_changed of a node without context is not reliable + # if the tree presented multiple times attribute will be true only in first execution + # so this should let to know next nodes that they also should be updated + if not node.is_input_linked: + for next_n in node.next_nodes: + if next_n.is_input_linked or BlNode(next_n.bl_tween).is_debug_node: + del next_n.is_input_changed + + except CancelError as e: + node.is_updated = False + node_error = e + except Exception as e: + node.is_updated = False + node_error = e + log_error(e) + return node_error diff --git a/core/handlers.py b/core/handlers.py index a58581d052..8b82df2009 100644 --- a/core/handlers.py +++ b/core/handlers.py @@ -82,9 +82,6 @@ def sv_handler_undo_post(scene): undo_handler_node_count['sv_groups'] = 0 - import sverchok.core.group_handlers as gh - gh.GroupContextTrees.reset_data() # todo repeat the logic from main tree? - # ideally we would like to recalculate all from scratch # but with heavy trees user can be scared of pressing undo button # I consider changes in tree topology as most common case @@ -152,11 +149,7 @@ def sv_pre_load(scene): clear_all_socket_cache() sv_clean(scene) - import sverchok.core.group_handlers as gh - gh.NodesStatuses.reset_data() - gh.GroupContextTrees.reset_data() import sverchok.core.main_tree_handler as mh - mh.NodesStatuses.reset_data() mh.ContextTrees.reset_data() diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index bfad82a2ab..471382e025 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -5,11 +5,13 @@ # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE +from __future__ import annotations + import gc -from collections import defaultdict +from contextlib import contextmanager from functools import partial from time import time -from typing import Dict, NamedTuple, Generator, Optional, Iterator, Tuple, Union +from typing import Dict, Generator, Optional, Iterator, Tuple, Union, NewType, List, TYPE_CHECKING import bpy from sverchok.core.sv_custom_exceptions import SvNoDataError, CancelError @@ -18,9 +20,15 @@ from sverchok.core.events import TreeEvent, GroupEvent from sverchok.utils.logging import debug, catch_log_error, log_error from sverchok.utils.tree_structure import Tree, Node -from sverchok.utils.handle_blender_data import BlTrees, BlTree +from sverchok.utils.handle_blender_data import BlTrees, BlTree, BlNode from sverchok.utils.profile import profile +if TYPE_CHECKING: + from sverchok.core.node_group import SvGroupTreeNode + + +Path = NewType('Path', str) # concatenation of group node ids + class TreeHandler: @@ -65,13 +73,26 @@ def send(event: TreeEvent): @staticmethod def get_error_nodes(bl_tree) -> Iterator[Optional[Exception]]: """Return map of bool values to group tree nodes where node has error if value is True""" + tree = ContextTrees.get(bl_tree, rebuild=False) for node in bl_tree.nodes: - yield NodesStatuses.get(node).error + if node.bl_idname in {'NodeReroute', 'NodeFrame'}: + yield None + continue + with tree.set_exec_context(): # tests shows good performance frequent use of the context manager + error = tree.nodes[node.name].error + # exit context manager before yielding otherwise it will block reading context dependent properties + yield error @staticmethod def get_update_time(bl_tree) -> Iterator[Optional[float]]: + tree = ContextTrees.get(bl_tree, rebuild=False) for node in bl_tree.nodes: - yield NodesStatuses.get(node).update_time + if node.bl_idname in {'NodeReroute', 'NodeFrame'}: + yield None + continue + with tree.set_exec_context(): + upd_time = tree.nodes[node.name].update_time + yield upd_time @staticmethod def get_cum_time(bl_tree) -> Iterator[Optional[float]]: @@ -161,17 +182,17 @@ def debug_run_task(cls): start_time = time() while (time() - start_time) < 0.15: # 0.15 is max timer frequency node = next(cls._handler) - node.bl_tween.use_custom_color = True - node.bl_tween.color = (0.7, 1.000000, 0.7) + if node is not None: + node.bl_tween.set_temp_color((0.7, 1.000000, 0.7)) + else: + return cls._last_node = node cls._report_progress(f'Pres "ESC" to abort, updating node "{node.name}"') except StopIteration: - if 'node' in vars(): - return from time import sleep - sleep(2) + sleep(1) cls.finish_task() @classmethod @@ -224,20 +245,22 @@ def global_updater(event_type: str) -> Generator[Node, None, None]: # update only trees which should be animated (for performance improvement in case of many trees) if event_type == TreeEvent.FRAME_CHANGE: if bl_tree.sv_animate: - was_changed = yield from tree_updater(bl_tree) + was_changed = yield from tree_updater(bl_tree, trees_ui_to_update) # tree should be updated any way elif event_type == TreeEvent.FORCE_UPDATE and 'FORCE_UPDATE' in bl_tree: del bl_tree['FORCE_UPDATE'] - was_changed = yield from tree_updater(bl_tree) + was_changed = yield from tree_updater(bl_tree, trees_ui_to_update) # this seems the event upon some changes in the tree, skip tree if the property is switched off else: if bl_tree.sv_process: - was_changed = yield from tree_updater(bl_tree) + was_changed = yield from tree_updater(bl_tree, trees_ui_to_update) # it has sense to call this here if you press update all button or creating group tree from selected if was_changed: + # if "DEBUG": + # yield None bl_tree.update_ui() # this only will update UI of main trees trees_ui_to_update.discard(bl_tree) # protection from double updating @@ -248,36 +271,37 @@ def global_updater(event_type: str) -> Generator[Node, None, None]: bl_tree.update_ui(*args) -def tree_updater(bl_tree) -> Generator[Node, None, bool]: +def tree_updater(bl_tree, trees_ui_to_update: set) -> Generator[Node, None, bool]: tree = ContextTrees.get(bl_tree) tree_output_changed = False - for node in tree.sorted_walk(tree.output_nodes): - can_be_updated = all(n.is_updated for n in node.last_nodes) - if not can_be_updated: - # here different logic can be implemented but for this we have to know if is there any output of the node - # we could leave the node as updated and don't broke work of the rest forward nodes - # but if the node does not have any output all next nodes will gen NoDataError what is horrible - node.is_updated = False - node.is_output_changed = False - continue + with tree.set_exec_context(): + for node in tree.sorted_walk(tree.output_nodes): + can_be_updated = all(n.is_updated for n in node.last_nodes) + if not can_be_updated: + # here different logic can be implemented but for this we have to know if is there any output of the node + # we could leave the node as updated and don't broke work of the rest forward nodes + # but if the node does not have any output all next nodes will gen NoDataError what is horrible + node.is_updated = False + node.is_output_changed = False + continue - if hasattr(node.bl_tween, 'updater'): - updater = group_node_updater(node) - elif hasattr(node.bl_tween, 'process'): - updater = node_updater(node) - else: - updater = empty_updater(node, error=None) + if hasattr(node.bl_tween, 'updater'): + updater = group_node_updater(node, trees_ui_to_update) + elif hasattr(node.bl_tween, 'process'): + updater = node_updater(node) + else: + updater = empty_updater(node, error=None) - # update node with sub update system, catch statistic - start_time = time() - node_error = yield from updater - update_time = (time() - start_time) + # update node with sub update system, catch statistic + start_time = time() + node_error = yield from updater + update_time = (time() - start_time) - if node.is_output_changed or node_error: - stat = NodeStatistic(node_error, None if node_error else update_time) - NodesStatuses.set(node.bl_tween, stat) - tree_output_changed = True + if node.is_output_changed or node_error: + node.error = node_error + node.update_time = None if node_error else update_time + tree_output_changed = True return tree_output_changed @@ -287,43 +311,30 @@ class ContextTrees: _trees: Dict[str, Tree] = dict() @classmethod - def get(cls, bl_tree): - """Return caught tree or new if the tree was not build yet""" + def get(cls, bl_tree, rebuild=True): + """Return caught tree. If rebuild is true it will try generate new tree if it was not build yet or changed""" tree = cls._trees.get(bl_tree.tree_id) # new tree, all nodes are outdated if tree is None: - tree = Tree(bl_tree) - cls._trees[bl_tree.tree_id] = tree + if rebuild: + tree = Tree(bl_tree) + cls._trees[bl_tree.tree_id] = tree + else: + raise RuntimeError(f"Tree={bl_tree} was never executed yet") # topology of the tree was changed and should be updated + # Two reasons why always new tree is generated - it's simpler and new tree keeps fresh references to the nodes elif not tree.is_updated: - tree = cls._update_tree(bl_tree) - cls._trees[bl_tree.tree_id] = tree + if rebuild: + tree = Tree(bl_tree) + cls._update_topology_status(tree) + cls._trees[bl_tree.tree_id] = tree + else: + raise RuntimeError(f"Tree={tree} is outdated") return tree - @classmethod - def _update_tree(cls, bl_tree): - """ - This method will generate new tree, copy is_updates status from previous tree - and update 'is_input_changed' node attribute according topological changes relatively previous call - Two reasons why always new tree is generated - it's simpler and new tree keeps fresh references to the nodes - """ - new_tree = Tree(bl_tree) - - # copy is_updated attribute - if new_tree.id in cls._trees: - old_tree = cls._trees[new_tree.id] - for node in new_tree.nodes: - if node.name in old_tree.nodes: - node.is_updated = old_tree.nodes[node.name].is_updated - - # update is_input_changed attribute - cls._update_topology_status(new_tree) - - return new_tree - @classmethod def mark_tree_outdated(cls, bl_tree): """Whenever topology of a tree is changed this method should be called.""" @@ -332,7 +343,7 @@ def mark_tree_outdated(cls, bl_tree): tree.is_updated = False @classmethod - def mark_nodes_outdated(cls, bl_tree, bl_nodes): + def mark_nodes_outdated(cls, bl_tree, bl_nodes, context=''): """It will try to mark given nodes as to be recalculated. If node won't be found status of the tree will be changed to outdated""" if bl_tree.tree_id not in cls._trees: @@ -341,7 +352,11 @@ def mark_nodes_outdated(cls, bl_tree, bl_nodes): tree = cls._trees[bl_tree.tree_id] for bl_node in bl_nodes: try: - tree.nodes[bl_node.name].is_updated = False + if context: + with tree.set_exec_context(context): + tree.nodes[bl_node.name].is_updated = False + else: + del tree.nodes[bl_node.name].is_updated # it means that generated tree does no have given node and should be recreated by next request except KeyError: @@ -356,28 +371,52 @@ def reset_data(cls, bl_tree=None): Also single tre can be added, in this case only it will be deleted (it's going to be used in force update) """ if bl_tree and bl_tree.tree_id in cls._trees: + cls._trees[bl_tree.tree_id].delete() del cls._trees[bl_tree.tree_id] else: + for tree in cls._trees.values(): + tree.delete() cls._trees.clear() @classmethod - def calc_cam_update_time(cls, bl_tree) -> dict: + def calc_cam_update_time(cls, bl_tree, context='') -> dict: cum_time_nodes = dict() if bl_tree.tree_id not in cls._trees: return cum_time_nodes tree = cls._trees[bl_tree.tree_id] - for node in tree.sorted_walk(tree.output_nodes): - update_time = NodesStatuses.get(node.bl_tween).update_time - if update_time is None: # error node? - cum_time_nodes[node.bl_tween] = None - continue - if len(node.last_nodes) > 1: - cum_time = sum(NodesStatuses.get(n.bl_tween).update_time for n in tree.sorted_walk([node]) - if NodesStatuses.get(n.bl_tween).update_time is not None) - else: - cum_time = sum(cum_time_nodes.get(n.bl_tween, 0) for n in node.last_nodes) + update_time - cum_time_nodes[node.bl_tween] = cum_time + with tree.set_exec_context(context): + for node in tree.sorted_walk(tree.output_nodes): + if node.update_time is None: # error node? + cum_time_nodes[node.bl_tween] = None + continue + if len(node.last_nodes) > 1: + cum_time = sum(n.update_time for n in tree.sorted_walk([node]) if n.update_time is not None) + else: + cum_time = sum(cum_time_nodes.get(n.bl_tween, 0) for n in node.last_nodes) + node.update_time + cum_time_nodes[node.bl_tween] = cum_time + return cum_time_nodes + + @classmethod + def calc_cam_update_time_group(cls, bl_tree, group_nodes: List[SvGroupTreeNode]) -> dict: + cum_time_nodes = dict() + if bl_tree.tree_id not in cls._trees: + return cum_time_nodes + + tree = cls._trees[bl_tree.tree_id] + out_nodes = [n for n in tree.nodes if BlNode(n.bl_tween).is_debug_node] + out_nodes.extend([tree.nodes.active_output] if tree.nodes.active_output else []) + for node in tree.sorted_walk(out_nodes): + path = PathManager.generate_path(group_nodes) + with tree.set_exec_context(path): + if node.update_time is None: # error node? + cum_time_nodes[node.bl_tween] = None + continue + if len(node.last_nodes) > 1: + cum_time = sum(n.update_time for n in tree.sorted_walk([node]) if n.update_time is not None) + else: + cum_time = sum(cum_time_nodes.get(n.bl_tween, 0) for n in node.last_nodes) + node.update_time + cum_time_nodes[node.bl_tween] = cum_time return cum_time_nodes @classmethod @@ -398,50 +437,25 @@ def _update_topology_status(cls, new_tree: Tree): # this is only because some nodes calculated data only if certain output socket is connected # ideally we would not like ot make previous node outdated, but it requires changes in many nodes if not has_old_from_socket_links: - link.from_node.is_input_changed = True + del link.from_node.is_input_changed else: - link.to_node.is_input_changed = True + del link.to_node.is_input_changed removed_links = old_tree.links - new_tree.links for link in removed_links: if link.to_node in new_tree.nodes: - new_tree.nodes[link.to_node.name].is_input_changed = True - - -class NodeStatistic(NamedTuple): - """ - Statistic should be kept separately for each node - because each node can have 10 or even 100 of different statistic profiles according number of group nodes using it - """ - error: Optional[Exception] = None - update_time: float = None # sec - - -class NodesStatuses: - """It keeps node attributes""" - NodeId = str - _statuses: Dict[NodeId, NodeStatistic] = defaultdict(NodeStatistic) - - @classmethod - def get(cls, bl_node) -> NodeStatistic: - return cls._statuses[bl_node.node_id] - - @classmethod - def set(cls, bl_node, stat: NodeStatistic): - node_id = bl_node.node_id - cls._statuses[node_id] = stat + del new_tree.nodes[link.to_node.name].is_input_changed - @classmethod - def reset_data(cls): - """This method should be called before opening new file to free all statistic data""" - cls._statuses.clear() - -class CancelError(Exception): - """Aborting tree evaluation by user""" +class PathManager: + @staticmethod + def generate_path(group_nodes: List[SvGroupTreeNode]) -> Path: + """path is ordered collection group node ids + max length of path should be no more then number of base trees of most nested group node + 1""" + return Path('.'.join(n.node_id for n in group_nodes)) -def node_updater(node: Node, *args) -> Generator[Node, None, Optional[Exception]]: +def node_updater(node: Node) -> Generator[Node, None, Optional[Exception]]: """The node should has process method, all previous nodes should be updated""" node_error = None @@ -453,10 +467,10 @@ def node_updater(node: Node, *args) -> Generator[Node, None, Optional[Exception] if should_be_updated: try: yield node - get_sock_data(node) - node.bl_tween.process(*args) - node.is_updated = True - node.is_output_changed = True + with handle_node_data(node): + node.bl_tween.process() + node.is_updated = True + node.is_output_changed = True except CancelError as e: node.is_updated = False node_error = e @@ -467,22 +481,22 @@ def node_updater(node: Node, *args) -> Generator[Node, None, Optional[Exception] return node_error -def group_node_updater(node: Node) -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: +def group_node_updater(node: Node, trees_ui_to_update: set) -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: """The node should have updater attribute""" previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) should_be_updated = (not node.is_updated or node.is_input_changed or previous_nodes_are_changed) - yield node # yield groups node so it be colored by node Updater if necessary - if should_be_updated: - get_sock_data(node) - updater = node.bl_tween.updater(is_input_changed=should_be_updated) - is_output_changed, out_error = yield from updater + updater = node.bl_tween.updater(is_input_changed=should_be_updated, trees_ui_to_update=trees_ui_to_update) + with handle_node_data(node): # it's is redundant if the node group has no changes + is_output_changed, out_error = yield from updater + if is_output_changed or out_error: + yield node # yield groups node so it be colored by node Updater if necessary node.is_input_changed = False node.is_updated = not out_error node.is_output_changed = is_output_changed return out_error -def empty_updater(node: Node = None, **kwargs): +def empty_updater(node: Node = None, **kwargs): # todo to remove there is no reroute nodes in trees anymore """Reroutes, frame nodes, empty updaters which do nothing, set node in correct state returns given kwargs (only their values) like error=None, is_updated=True""" if node: # ideally we would like always get first argument as node but group updater does not posses it @@ -495,30 +509,37 @@ def empty_updater(node: Node = None, **kwargs): yield -def get_sock_data(node: Node): - """Get data from previous nodes. Should be called before given node execution""" +@contextmanager +def handle_node_data(node: Node): + """Any node should be executed inside this context manager. It supply node with data and save output node data + Also it makes data conversion if it is needed""" + + # before execution the data should be put into input sockets + # the storage of the data is in output sockets and is dependent on context + # context should be set before the function execution for in_sock in node.inputs: for out_sock in in_sock.linked_sockets: + data = out_sock.data - # reroute nodes should be treated separately, they does not have socket catch - # and data should be searched in previous nodes - # it would be nice to standardize their API but its seems impossible without node.copy trigger - if out_sock.node.bl_tween.bl_idname == 'NodeReroute': - while True: - prev_socks = out_sock.node.inputs[0].linked_sockets - if prev_socks: - out_sock = prev_socks[0] - if out_sock.node.bl_tween.bl_idname != 'NodeReroute': - break - else: - raise SvNoDataError(in_sock.bl_tween) - - # get data from normal node - data = out_sock.bl_tween.sv_get() + # cast data from one socket type to another if out_sock.bl_tween.bl_idname != in_sock.bl_tween.bl_idname: implicit_conversions = ConversionPolicies.get_conversion(in_sock.bl_tween.default_conversion_name) data = implicit_conversions.convert(in_sock.bl_tween, out_sock.bl_tween, data) - in_sock.bl_tween.sv_set(data) + + # save data to input socket + in_sock.bl_tween.sv_set(data) # data should be saved without context to be able to read by node + + # pass flow for node execution + yield None + + # after node was executed the data should be reputed into appropriate place according to execution context + # this redundant step in main trees and have only sense inside node groups + for out_sock in node.outputs: + try: + if hasattr(out_sock.bl_tween, 'sv_get'): # in case the node is group input one + out_sock.data = out_sock.bl_tween.sv_get() + except SvNoDataError: + pass @post_load_call diff --git a/core/node_group.py b/core/node_group.py index 2007f8418e..b23b8d5517 100644 --- a/core/node_group.py +++ b/core/node_group.py @@ -17,11 +17,11 @@ from sverchok.data_structure import extend_blender_class from mathutils import Vector -from sverchok.core.group_handlers import MainHandler, NodeIdManager +from sverchok.core.group_handlers import MainHandler from sverchok.core.events import GroupEvent from sverchok.utils.tree_structure import Tree, Node from sverchok.utils.sv_node_utils import recursive_framed_location_finder -from sverchok.utils.handle_blender_data import BlNode, BlTree, BlTrees +from sverchok.utils.handle_blender_data import BlNode, BlTrees from sverchok.utils.logging import catch_log_error from sverchok.node_tree import UpdateNodes, SvNodeTreeCommon, SverchCustomTreeNode @@ -211,9 +211,7 @@ def parent_nodes(self) -> Iterator['SvGroupTreeNode']: yield node def update_ui(self, group_nodes_path: List['SvGroupTreeNode']): - """updating tree contextual information -> node colors, objects number in sockets, debugger nodes""" - self.handler.send(GroupEvent(GroupEvent.EDIT_GROUP_NODE, group_nodes_path)) - + """updating tree contextual information -> node colors, objects number in sockets""" nodes_errors = self.handler.get_error_nodes(group_nodes_path) to_show_update_time = group_nodes_path[0].id_data.sv_show_time_nodes time_mode = group_nodes_path[0].id_data.show_time_mode @@ -224,12 +222,7 @@ def update_ui(self, group_nodes_path: List['SvGroupTreeNode']): update_time = cycle([None]) for node, error, update in zip(self.nodes, nodes_errors, update_time): if hasattr(node, 'update_ui'): - node.update_ui(error, update, NodeIdManager.extract_node_id(node)) - - # update debug nodes - if BlNode(node).is_debug_node: - with catch_log_error(): - node.process() + node.update_ui(error, update) def get_update_path(self) -> List['SvGroupTreeNode']: """ @@ -399,7 +392,8 @@ def process(self): # todo to remove raise error def updater(self, group_nodes_path: Optional[List['SvGroupTreeNode']] = None, - is_input_changed: bool = True) -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: + is_input_changed: bool = True, + trees_ui_to_update: set = None) -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: """ This method should be called by group tree handler is_input_changed should be False if update is called just for inspection of inner changes @@ -420,7 +414,8 @@ def updater(self, group_nodes_path: Optional[List['SvGroupTreeNode']] = None, input_node = self.active_input() if is_input_changed else None return self.node_tree.handler.update(GroupEvent(GroupEvent.GROUP_NODE_UPDATE, group_nodes_path, - [input_node] if input_node else [])) + [input_node] if input_node else []), + trees_ui_to_update or set()) def active_input(self) -> Optional[bpy.types.Node]: # https://developer.blender.org/T82350 @@ -444,8 +439,6 @@ def update(self): update_ui = UpdateNodes.update_ui # don't want to inherit from the class (at least now) def free(self): - # This is inevitable evil cause of flexible nature of node_ids inside group trees - node_id = NodeIdManager.extract_node_id(self) if BlTree(self.id_data).is_group_tree else self.node_id self.update_ui() @@ -828,7 +821,8 @@ def execute(self, context): context.space_data.path.append(sub_tree, node=group_node) sub_tree.group_node_name = group_node.name group_nodes_path = sub_tree.get_update_path() - sub_tree.update_ui(group_nodes_path) + sub_tree.handler.send(GroupEvent(GroupEvent.EDIT_GROUP_NODE, group_nodes_path, + (n for n in sub_tree.nodes if BlNode(n).is_debug_node))) # todo make protection from editing the same trees in more then one area # todo add the same logic to exit from tree operator return {'FINISHED'} diff --git a/core/socket_data.py b/core/socket_data.py index 769049ee7a..3d283f868d 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -19,11 +19,13 @@ """For internal usage of the sockets module""" from collections import defaultdict -from typing import Dict, NewType, Union, Optional +from typing import Dict, NewType, Optional -SockAddress = NewType('SockAddress', str) +from sverchok.core.sv_custom_exceptions import SvNoDataError + +SockId = NewType('SockId', str) SockContext = NewType('SockContext', str) # socket can have multiple values in case it used inside node group -DataAddress = Dict[SockAddress, Dict[Union[SockContext, None], Optional[list]]] +DataAddress = Dict[SockId, Dict[SockContext, Optional[list]]] socket_data_cache: DataAddress = defaultdict(lambda: defaultdict(lambda: None)) @@ -43,74 +45,37 @@ def sv_deep_copy(lst): def sv_forget_socket(socket): """deletes socket data from cache""" try: - del socket_data_cache[_get_sock_address(socket)] + del socket_data_cache[socket.socket_id] except KeyError: pass -def sv_set_socket(socket, data, context: SockContext = None): +def sv_set_socket(socket, data, context: SockContext = ''): """sets socket data for socket""" - socket_data_cache[_get_sock_address(socket)][context] = data + socket_data_cache[socket.socket_id][context] = data -def sv_get_socket(socket, deepcopy=True, context: SockContext = None): +def sv_get_socket(socket, deepcopy=True, context: SockContext = ''): """gets socket data from socket, if deep copy is True a deep copy is make_dep_dict, to increase performance if the node doesn't mutate input set to False and increase performance substanstilly """ - data = socket_data_cache[_get_sock_address(socket)][context] + data = socket_data_cache[socket.socket_id][context] if data is not None: return sv_deep_copy(data) if deepcopy else data else: raise SvNoDataError(socket) -def _get_sock_address(sock) -> SockAddress: - return sock.id_data.tree_id + sock.socket_id - - -class SvNoDataError(LookupError): - def __init__(self, socket=None, node=None, msg=None): - - self.extra_message = msg if msg else "" - - if node is None and socket is not None: - node = socket.node - self.node = node - self.socket = socket - - super(LookupError, self).__init__(self.get_message()) - - def get_message(self): - if self.extra_message: - return f"node {self.socket.node.name} (socket {self.socket.name}) {self.extra_message}" - if not self.node and not self.socket: - return "SvNoDataError" - else: - return f"No data passed into socket '{self.socket.name}'" - - def __repr__(self): - return self.get_message() - - def __str__(self): - return repr(self) - - def __unicode__(self): - return repr(self) - - def __format__(self, spec): - return repr(self) - - -def get_output_socket_data(node, output_socket_name, context: SockContext = None): +def get_output_socket_data(node, output_socket_name, context: SockContext = ''): """ This method is intended to usage in internal tests mainly. Get data that the node has written to the output socket. Raises SvNoDataError if it hasn't written any. """ socket = node.inputs[output_socket_name] # todo why output? - sock_address = _get_sock_address(socket) + sock_address = socket.socket_id if sock_address in socket_data_cache and context in socket_data_cache[sock_address]: return socket_data_cache[sock_address][context] else: diff --git a/core/sockets.py b/core/sockets.py index 9cf3851fbf..6b6d3fc046 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -355,7 +355,7 @@ def other(self): @property def socket_id(self): """Id of socket used by data_cache""" - return str(hash(self.node.node_id + self.identifier)) + return str(hash(self.node.node_id + self.identifier + ('o' if self.is_output else 'i'))) @property def index(self): @@ -380,27 +380,26 @@ def hide_safe(self, value): self.hide = value - def sv_get(self, default=..., deepcopy=True, context=None): + def sv_get(self, default=..., deepcopy=True): # todo should be removed, data should path directly to process method """ The method is used for getting socket data In most cases the method should not be overridden Also a socket can use its default_property Order of getting data (if available): - 1. written socket data (for output sockets this is the only option) + 1. written socket data 2. node default property 3. socket default property 4. script default property 5. Raise no data error :param default: script default property :param deepcopy: in most cases should be False for efficiency but not in cases if input data will be modified - :param context: provide this in case the node can be evaluated several times in different contexts :return: data bound to the socket """ if self.is_output: - return sv_get_socket(self, False, context) + return sv_get_socket(self, False) if self.is_linked: - return sv_get_socket(self, deepcopy, context) + return sv_get_socket(self, deepcopy) prop_name = self.get_prop_name() if prop_name: @@ -416,11 +415,11 @@ def sv_get(self, default=..., deepcopy=True, context=None): raise SvNoDataError(self) - def sv_set(self, data, context: str = None): + def sv_set(self, data): # todo should be removed """Set data, provide context in case the node can be evaluated several times in different context""" if self.is_output: data = self.postprocess_output(data) - sv_set_socket(self, data, context=context) + sv_set_socket(self, data) def sv_forget(self): """Delete socket memory""" @@ -529,13 +528,13 @@ def draw_label(text): def draw_color(self, context, node): return self.color - def update_objects_number(self, context=None): # todo should be context here? + def update_objects_number(self): # todo should be the method here? """ Should be called each time after process method of the socket owner It will update number of objects to show in socket labels """ try: - self.objects_number = len(self.sv_get(deepcopy=False, default=[], context=context)) + self.objects_number = len(self.sv_get(deepcopy=False, default=[])) except LookupError: self.objects_number = 0 except Exception as e: diff --git a/core/update_system.py b/core/update_system.py index d2383f9f70..1f579e6f79 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -26,7 +26,6 @@ from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.utils.logging import warning, error, exception from sverchok.utils.profile import profile -from sverchok.core.socket_data import clear_all_socket_cache import sverchok import ast diff --git a/node_tree.py b/node_tree.py index c9451d4578..60f72f7521 100644 --- a/node_tree.py +++ b/node_tree.py @@ -16,7 +16,6 @@ from sverchok.core.sv_custom_exceptions import SvNoDataError from sverchok.core.events import TreeEvent from sverchok.core.main_tree_handler import TreeHandler -from sverchok.core.group_handlers import NodeIdManager from sverchok.data_structure import classproperty, post_load_call from sverchok.utils import get_node_class_reference from sverchok.utils.sv_node_utils import recursive_framed_location_finder @@ -27,7 +26,6 @@ from sverchok.ui import color_def from sverchok.ui.nodes_replacement import set_inputs_mapping, set_outputs_mapping from sverchok.ui import bgl_callback_nodeview as sv_bgl -from sverchok.utils.handle_blender_data import BlTree class SvNodeTreeCommon: @@ -241,14 +239,15 @@ def sv_new_input(self, socket_type, name, **attrib_dict): def free(self): """Called upon the node removal""" + # custom free function self.sv_free() + # free sockets memory for s in self.outputs: s.sv_forget() - # This is inevitable evil cause of flexible nature of node_ids inside group trees - node_id = NodeIdManager.extract_node_id(self) if BlTree(self.id_data).is_group_tree else self.node_id - self.update_ui(node_id=node_id) + # remove tree space drawings + self.update_ui() def copy(self, original): """Called upon the node being copied""" @@ -265,7 +264,7 @@ def update(self): self.sv_update() - def update_ui(self, error=None, update_time=None, node_id=None): + def update_ui(self, error=None, update_time=None): """updating tree contextual information -> node colors, text node_id only for usage of a group tree""" sv_settings = bpy.context.preferences.addons[sverchok.__name__].preferences @@ -273,23 +272,22 @@ def update_ui(self, error=None, update_time=None, node_id=None): no_data_color = sv_settings.no_data_color error_pref = "error" update_pref = "update_time" - node_id = node_id or self.node_id # inevitable evil # update error colors if error is not None: color = no_data_color if isinstance(error, SvNoDataError) else exception_color self.set_temp_color(color) - sv_bgl.draw_text(self, repr(error), error_pref + node_id, color, 1.3, "UP") + sv_bgl.draw_text(self, repr(error), error_pref + self.node_id, color, 1.3, "UP") else: - sv_bgl.callback_disable(error_pref + node_id) + sv_bgl.callback_disable(error_pref + self.node_id) self.set_temp_color() # show update timing if update_time is not None: update_time = int(update_time * 1000) - sv_bgl.draw_text(self, f'{update_time}ms', update_pref + node_id, align="UP", dynamic_location=False) + sv_bgl.draw_text(self, f'{update_time}ms', update_pref + self.node_id, align="UP", dynamic_location=False) else: - sv_bgl.callback_disable(update_pref + node_id) + sv_bgl.callback_disable(update_pref + self.node_id) # update object numbers for s in chain(self.inputs, self.outputs): diff --git a/utils/tree_structure.py b/utils/tree_structure.py index 09fffb15d0..3899dfd536 100644 --- a/utils/tree_structure.py +++ b/utils/tree_structure.py @@ -8,12 +8,16 @@ from __future__ import annotations -from collections import Mapping -from typing import List, Iterable, TypeVar, TYPE_CHECKING, Dict, Any, Generic, Optional, Union +from collections import Mapping, defaultdict +from contextlib import contextmanager +from functools import wraps +from typing import List, Iterable, TypeVar, TYPE_CHECKING, Dict, Any, Generic, Optional, Union, NewType import bpy import sverchok.utils.tree_walk as tw +from sverchok.core.socket_data import sv_get_socket, sv_set_socket +from sverchok.utils.handle_blender_data import BlNode if TYPE_CHECKING: from sverchok.core.node_group import SvGroupTree @@ -21,6 +25,17 @@ SvNode = Union[SverchCustomTreeNode, bpy.types.Node] +def _context_dependent(func): + """Decorator for context dependent methods and properties. Raise error if context is not determined + Decorated methods should have context argument and the class should have exec_context property""" + @wraps(func) + def inner(self, *args, **kwargs): + if self.exec_context is None: + raise RuntimeError("Before execution this method/property execution context should be determined") + return func(self, *args, **kwargs) + return inner + + class Node(tw.Node): def __init__(self, name: str, index: int, tree: Tree, bl_node): self.name = name @@ -29,10 +44,7 @@ def __init__(self, name: str, index: int, tree: Tree, bl_node): self._outputs: List[Socket] = [] self._index = index self._tree = tree - - self.is_input_changed = False - self.is_updated = False - self.is_output_changed = False + self._is_input_linked = None # has links lead to group input nodes # cash self.bl_tween = bl_node @@ -42,6 +54,68 @@ def __init__(self, name: str, index: int, tree: Tree, bl_node): # """Quite expansive function, 1ms = 800 calls, it's better to cash, is potentially dangerous""" # return self._tree.bl_tween.nodes[self._index] + @property + def id(self): + return self.bl_tween.node_id + + @property + @_context_dependent + def is_updated(self): + return ContextAttributes.get(self.id, 'is_updated', False, self.exec_context) + + @is_updated.setter + @_context_dependent + def is_updated(self, status): + ContextAttributes.set(self.id, 'is_updated', status, self.exec_context) + + @is_updated.deleter + def is_updated(self): + ContextAttributes.del_attr_data(self.id, 'is_updated') + + @property + @_context_dependent + def is_input_changed(self): + return ContextAttributes.get(self.id, 'is_input_changed', True, self.exec_context) + + @is_input_changed.setter + @_context_dependent + def is_input_changed(self, status): + ContextAttributes.set(self.id, 'is_input_changed', status, self.exec_context) + + @is_input_changed.deleter + def is_input_changed(self): + ContextAttributes.del_attr_data(self.id, 'is_input_changed') + + @property + @_context_dependent + def is_output_changed(self): + return ContextAttributes.get(self.id, 'is_output_changed', True, self.exec_context) + + @is_output_changed.setter + @_context_dependent + def is_output_changed(self, status): + ContextAttributes.set(self.id, 'is_output_changed', status, self.exec_context) + + @property + @_context_dependent + def error(self) -> Exception: + return ContextAttributes.get(self.id, 'error', None, self.exec_context) + + @error.setter + @_context_dependent + def error(self, err: Exception): + ContextAttributes.set(self.id, 'error', err, self.exec_context) + + @property + @_context_dependent + def update_time(self) -> float: + return ContextAttributes.get(self.id, 'update_time', None, self.exec_context) + + @update_time.setter + @_context_dependent + def update_time(self, upd_time: float): + ContextAttributes.set(self.id, 'update_time', upd_time, self.exec_context) + @property def index(self): """Index of node location in Blender collection from which it was copied""" @@ -65,6 +139,18 @@ def last_nodes(self) -> Iterable[Node]: """Returns all nodes which are linked wia the node input sockets""" return {other_s.node for s in self.inputs for other_s in s.linked_sockets} + @property + def is_input_linked(self) -> bool: + if self._is_input_linked is None: # or should it raise an error instead? and force user to call method manually + self._tree.fill_is_input_linked() + return self._is_input_linked + + @property + def exec_context(self): + """Return tree path if node is input linked and is not a debug otherwise empty path is returned""" + return self._tree.exec_context if self.is_input_linked else '' + # return self._tree.exec_context if not BlNode(self.bl_tween).is_debug_node and self.is_input_linked else '' + def get_bl_node(self, tree: SvGroupTree) -> bpy.types.Node: """ Will return the node from given tree with the same name @@ -117,6 +203,7 @@ def __init__(self, bl_tree: SvGroupTree): self._tree_id = bl_tree.tree_id self._nodes = NodesCollection(bl_tree, self) self._links = LinksCollection(bl_tree, self) + self._exec_context = None # should be given to use tree data dependent on context (socket data, stats) # if the tree is created in the same time with the class initialization (loading file) # the index of the tree in node_groups collection will be not found (-1) @@ -141,6 +228,31 @@ def nodes(self) -> NodesCollection: def links(self) -> LinksCollection: return self._links + def fill_is_input_linked(self): # it can get type of input nodes optionally later + for node in self.nodes: + node._is_input_linked = False + for node in self.bfs_walk([self.nodes.active_input] if self.nodes.active_output else []): + node._is_input_linked = True + + @contextmanager + def set_exec_context(self, context: str = ''): # todo should be context type imported? + if self._exec_context is not None: + raise RuntimeError("Tree already has execution context") + self._exec_context = context + try: + yield None + finally: + self._exec_context = None + + @property + def exec_context(self): + return self._exec_context + + def delete(self): + """Free context data""" + for node in self.nodes: + ContextAttributes.del_obj_data(node.id) + def _handle_wifi_nodes(self): """The idea is to convert wifi nodes into regular nodes with sockets and links between them""" # todo the code is very bad and should be removed later wifi node refactoring @@ -206,10 +318,13 @@ def __sub__(self, other) -> List[Element]: class NodesCollection(TreeCollections[NodeType]): def __init__(self, bl_tree: SvGroupTree, tree: Tree): + """Generate Python representation of Blender nodes. Reroute and frame nodes are ignored""" super().__init__() self._active_input: Optional[Node] = None self._active_output: Optional[Node] = None for i, bl_node in enumerate(bl_tree.nodes): + if bl_node.bl_idname in {'NodeReroute', 'NodeFrame'}: + continue node = Node.from_bl_node(bl_node, i, tree) self._dict[bl_node.name] = node @@ -230,7 +345,15 @@ def active_output(self) -> Optional[Node]: class LinksCollection(TreeCollections): def __init__(self, bl_tree: SvGroupTree, tree: Tree): + """Generate Python representation of Blender links. Reroute nodes are thrown from the tree model. + Muted links are ignored""" super().__init__() + + # helping data structure for fast link search + from_node_links = defaultdict(list) + for bl_link in bl_tree.links: + from_node_links[bl_link.from_node].append(bl_link) + for i, bl_link in enumerate(bl_tree.links): # new in 2.93, it is the same as if there was no the link (is_hidden was added before 2.93) @@ -238,13 +361,34 @@ def __init__(self, bl_tree: SvGroupTree, tree: Tree): # or bl_link.is_hidden: # it does not call update method of a tree https://developer.blender.org/T89109 continue - from_node = tree.nodes[bl_link.from_node.name] - from_socket = from_node.get_output_socket(bl_link.from_socket.identifier) - to_node = tree.nodes[bl_link.to_node.name] - to_socket = to_node.get_input_socket(bl_link.to_socket.identifier) + # link from reroute node to be ignored + from_bl_node = bl_link.from_node + if from_bl_node.bl_idname == 'NodeReroute': + continue - self._dict[(bl_link.from_node.name, bl_link.from_socket.identifier, - bl_link.to_node.name, bl_link.to_socket.identifier)] = Link(from_socket, to_socket, i) + # link to normal node should be found + to_bl_node = bl_link.to_node + if to_bl_node.bl_idname == 'NodeReroute': + next_links = from_node_links[bl_link.to_node].copy() + to_links = [] + while next_links: + next_link = next_links.pop() + if next_link.to_node.bl_idname == 'NodeReroute': + next_links.extend(from_node_links[next_link.to_node]) + else: + to_links.append(next_link) + else: + to_links = [bl_link] + + # generate link(s) + for to_link in to_links: + from_node = tree.nodes[bl_link.from_node.name] + from_socket = from_node.get_output_socket(bl_link.from_socket.identifier) + to_node = tree.nodes[to_link.to_node.name] + to_socket = to_node.get_input_socket(to_link.to_socket.identifier) + + self._dict[(from_node.name, from_socket.identifier, + to_node.name, to_socket.identifier)] = Link(from_socket, to_socket, i) def __iter__(self) -> Iterable[Link]: return super().__iter__() @@ -273,6 +417,20 @@ def node(self) -> Node: def links(self) -> List[Link]: return self._links + @property + def exec_context(self): + return self._node.exec_context + + @property + @_context_dependent + def data(self): + return sv_get_socket(self.bl_tween, False, self.exec_context) + + @data.setter + @_context_dependent + def data(self, data): + sv_set_socket(self.bl_tween, data, self.exec_context) + def get_bl_socket(self, bl_tree: SvGroupTree) -> bpy.types.NodeSocket: """Search socket in given tree by its identifier""" bl_node = self.node.get_bl_node(bl_tree) @@ -322,3 +480,48 @@ def to_node(self) -> Node: def __repr__(self): return f'FROM "{self.from_node.name}.{self.from_socket.identifier}" ' \ f'TO "{self.to_node.name}.{self.to_socket.identifier}"' + + +class ContextAttributes: + """It keeps attributes which should be preserved between tree evaluations + also it should keep those attributes which can have different values depending on context execution + first scenario related with nature of Blender tree data structure which each time should be converted into + Python data structure for efficient search + second scenario is related with nature of node groups where a node can have different input dependent on + a group node from which execution has began""" + + ObjectId = NewType('ObjectId', str) + AttrName = NewType('AttrName', str) + Context = NewType('Context', str) # context id + DataAddress = Dict[ObjectId, Dict[AttrName, Dict[Context, Any]]] + _socket_data_cache: DataAddress = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: None))) + + @classmethod + def set(cls, object_id: ObjectId, attr_name: AttrName, data, context: Context = ''): + """Save data of attribute of an object. Context allow ot have multiple values per attribute""" + cls._socket_data_cache[object_id][attr_name][context] = data + + @classmethod + def get(cls, object_id: ObjectId, attr_name: AttrName, default=..., context: Context = ''): + """Get saved data of attribute of given object. Context determines multiple values for the same attribute""" + data = cls._socket_data_cache[object_id][attr_name][context] + if data is None and default is ...: + raise LookupError("Given object does not have any data") + else: + return default if data is None else data + + @classmethod + def del_obj_data(cls, object_id: ObjectId): + """Deletes all attributes of given object""" + try: + del cls._socket_data_cache[object_id] + except KeyError: + pass + + @classmethod + def del_attr_data(cls, object_id: ObjectId, attr_name: AttrName): + """Delete all data of attribute of given object""" + try: + del cls._socket_data_cache[object_id][attr_name] + except KeyError: + pass From 7d1c1070417e1b585b0a5e94784afb391c681aa4 Mon Sep 17 00:00:00 2001 From: Durman Date: Sat, 14 May 2022 20:05:44 +0400 Subject: [PATCH 05/25] more efficient approach for animation --- core/__init__.py | 2 +- core/events.py | 11 ++- core/handlers.py | 25 ++++-- core/main_tree_handler.py | 23 ++++- core/simple_update_system.py | 170 +++++++++++++++++++++++++++++++++++ core/sockets.py | 7 +- node_tree.py | 25 +++--- nodes/list_struct/item.py | 10 +-- nodes/list_struct/split.py | 8 +- 9 files changed, 250 insertions(+), 31 deletions(-) create mode 100644 core/simple_update_system.py diff --git a/core/__init__.py b/core/__init__.py index 0ec299d942..a954cb7d2b 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -10,7 +10,7 @@ ] core_modules = [ - "sv_custom_exceptions", + "sv_custom_exceptions", "simple_update_system", "sockets", "socket_data", "handlers", "update_system", "main_tree_handler", "events", "node_group", "group_handlers" diff --git a/core/events.py b/core/events.py index 3f413d03ee..62ff78eafc 100644 --- a/core/events.py +++ b/core/events.py @@ -34,12 +34,21 @@ class TreeEvent: FORCE_UPDATE = 'force_update' # rebuild tree and reevaluate every node FRAME_CHANGE = 'frame_change' # unlike other updates this one should be un-cancellable SCENE_UPDATE = 'scene_update' # something was changed in the scene + FILE_RELOADED = 'file_reloaded' # New files was opened - def __init__(self, event_type: str, tree: SverchCustomTree, updated_nodes: Iterable[SvNode] = None, cancel=True): + def __init__(self, + event_type: str, + tree: SverchCustomTree, + updated_nodes: Iterable[SvNode] = None, + cancel=True, + is_frame_changed: bool = True, + is_animation_playing: bool = False): self.type = event_type self.tree = tree self.updated_nodes = updated_nodes self.cancel = cancel + self.is_frame_changed = is_frame_changed + self.is_animation_playing = is_animation_playing def __repr__(self): return f"" diff --git a/core/handlers.py b/core/handlers.py index dede7c1faf..5caebccd4f 100644 --- a/core/handlers.py +++ b/core/handlers.py @@ -3,6 +3,7 @@ from sverchok import old_nodes from sverchok import data_structure +from sverchok.core.events import TreeEvent from sverchok.core.socket_data import clear_all_socket_cache from sverchok.ui import bgl_callback_nodeview, bgl_callback_3dview from sverchok.utils import app_handler_ops @@ -95,13 +96,21 @@ def sv_handler_undo_post(scene): def sv_update_handler(scene): """ Update sverchok node groups on frame change events. + Jump from one frame to another: has_frame_changed=True, is_animation_playing=False + Scrubbing variant 1: has_frame_changed=True, is_animation_playing=True + Scrubbing variant 2(stop): has_frame_changed=True, is_animation_playing=False + Scrubbing variant 3: has_frame_changed=False, is_animation_playing=True + Scrubbing variant 4(stop): has_frame_changed=False, is_animation_playing=False + Playing animation: has_frame_changed=True, is_animation_playing=True + Playing animation(stop): has_frame_changed=False, is_animation_playing=False """ - if not has_frame_changed(scene): - return + is_playing = bpy.context.screen.is_animation_playing + is_frame_changed = has_frame_changed(scene) + # print(f"Frame changed: {is_frame_changed}, Animation is playing: {is_playing}") - for ng in sverchok_trees(): + for ng in sverchok_trees(): # Comparatively small overhead with 200 trees in a file with catch_log_error(): - ng.process_ani() + ng.process_ani(is_frame_changed, is_playing) @persistent @@ -150,7 +159,7 @@ def sv_pre_load(scene): sv_clean(scene) import sverchok.core.main_tree_handler as mh - mh.ContextTrees.reset_data() + mh.TreeHandler.send(TreeEvent(TreeEvent.FILE_RELOADED, tree=None)) @persistent @@ -210,6 +219,12 @@ def update_frame_change_mode(): @persistent def update_trees_scene_change(scene): + """When the Play Animation is on this trigger is executed once. Such event + should be suppressed because it repeats animation trigger. Whe Play + animation is on this and user changes something in scene this trigger is + only called if frame rate is equal to maximum.""" + if bpy.context.screen.is_animation_playing: + return for ng in BlTrees().sv_main_trees: ng.scene_update() diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index 642b665382..9fef0f6a25 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -22,6 +22,7 @@ from sverchok.utils.tree_structure import Tree, Node from sverchok.utils.handle_blender_data import BlTrees, BlTree, BlNode from sverchok.utils.profile import profile +import sverchok.core.simple_update_system as sus if TYPE_CHECKING: from sverchok.core.node_group import SvGroupTreeNode @@ -34,6 +35,7 @@ class TreeHandler: @staticmethod def send(event: TreeEvent): + # debug(event.type) # this should be first other wise other instructions can spoil the node statistic to redraw if NodesUpdater.is_running(): @@ -46,8 +48,9 @@ def send(event: TreeEvent): # This event can't be handled via NodesUpdater during animation rendering because new frame change event # can arrive before timer finishes its tusk. Or timer can start working before frame change is handled. if event.type == TreeEvent.FRAME_CHANGE: - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - profile(section="UPDATE")(lambda: list(global_updater(event.type)))() + sus.process_nodes(event) + # ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) + # profile(section="UPDATE")(lambda: list(global_updater(event.type)))() return # something changed in scene and it duplicates some tree events which should be ignored @@ -63,10 +66,12 @@ def send(event: TreeEvent): # mark given nodes as outdated elif event.type == TreeEvent.NODES_UPDATE: + sus.Tree.get(event.tree).reset_walk() ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) # it will find changes in tree topology and mark related nodes as outdated elif event.type == TreeEvent.TREE_UPDATE: + sus.Tree.reset_tree(event.tree) ContextTrees.mark_tree_outdated(event.tree) # force update @@ -74,6 +79,11 @@ def send(event: TreeEvent): ContextTrees.reset_data(event.tree) event.tree['FORCE_UPDATE'] = True + # new file opened + elif event.type == TreeEvent.FILE_RELOADED: + sus.Tree.reset_tree() + ContextTrees.reset_data() + # Unknown event else: raise TypeError(f'Detected unknown event - {event}') @@ -272,7 +282,7 @@ def global_updater(event_type: str) -> Generator[Node, None, None]: if was_changed: # if "DEBUG": # yield None - bl_tree.update_ui() # this only will update UI of main trees + update_ui(bl_tree) # this only will update UI of main trees trees_ui_to_update.discard(bl_tree) # protection from double updating # this only need to trigger scene changes handler again @@ -288,6 +298,13 @@ def global_updater(event_type: str) -> Generator[Node, None, None]: bl_tree.update_ui(*args) +def update_ui(tree): + nodes_errors = TreeHandler.get_error_nodes(tree) + update_time = (TreeHandler.get_cum_time(tree) if tree.show_time_mode == "Cumulative" + else TreeHandler.get_update_time(tree)) + tree.update_ui(nodes_errors, update_time) + + def tree_updater(bl_tree, trees_ui_to_update: set) -> Generator[Node, None, bool]: tree = ContextTrees.get(bl_tree) tree_output_changed = False diff --git a/core/simple_update_system.py b/core/simple_update_system.py new file mode 100644 index 0000000000..8f3fca14b3 --- /dev/null +++ b/core/simple_update_system.py @@ -0,0 +1,170 @@ +from collections import defaultdict, deque +from graphlib import TopologicalSorter +from time import perf_counter +from typing import Optional, Generator + +from bpy_types import Node, NodeSocket, NodeTree +from sverchok.core.events import TreeEvent +from sverchok.core.socket_conversions import ConversionPolicies +from sverchok.utils.profile import profile +from sverchok.utils.logging import log_error + + +UPDATE_KEY = "US_is_updated" +ERROR_KEY = "US_error" +TIME_KEY = "US_time" + + +@profile(section="UPDATE") +def process_nodes(event: TreeEvent): + if event.is_frame_changed: + for node, prev_socks in Tree.get(event.tree).walk(event.updated_nodes): + # node.set_temp_color([1, 1, 1]) # execution debug + try: + t = perf_counter() + prepare_input_data(prev_socks, node.inputs) + node.process() + node[UPDATE_KEY] = True + node[ERROR_KEY] = None + node[TIME_KEY] = perf_counter() - t + except Exception as e: + log_error(e) + node[UPDATE_KEY] = False + node[ERROR_KEY] = repr(e) + + if not event.is_animation_playing: + update_ui(event.tree) + + +def prepare_input_data(prev_socks, input_socks): + for ps, ns in zip(prev_socks, input_socks): + if ps is None: + continue + data = ps.sv_get() + + # cast data + if ps.bl_idname != ns.bl_idname: + implicit_conversion = ConversionPolicies.get_conversion(ns.default_conversion_name) + data = implicit_conversion.convert(ns, ps, data) + + ns.sv_set(data) + + +def update_ui(tree: NodeTree): + errors = (n.get(ERROR_KEY, None) for n in tree.nodes) + times = (n.get(TIME_KEY, 0) for n in tree.nodes) + tree.update_ui(errors, times) + + +class Tree: + """It catches some data for more efficient searches compare to Blender + tree data structure""" + _tree_catch: dict[str, 'Tree'] = dict() # the module should be auto-reloaded to prevent crashes + + def __init__(self, tree: NodeTree): + self._from_nodes: dict[Node, set[Node]] = defaultdict(set) + self._to_nodes: dict[Node, set[Node]] = defaultdict(set) + self._from_sock: dict[NodeSocket, NodeSocket] = dict() + self._sock_node: dict[NodeSocket, Node] = dict() + self.__walk_catch: Optional[tuple[Node, list[NodeSocket]]] = None + + for link in (li for li in tree.links if not li.is_muted): + self._from_nodes[link.to_node].add(link.from_node) + self._to_nodes[link.from_node].add(link.to_node) + self._from_sock[link.to_socket] = link.from_socket + self._sock_node[link.from_socket] = link.from_node + + self._remove_reroutes() + + @classmethod + def get(cls, tree: NodeTree): + if tree.tree_id not in cls._tree_catch: + cls._tree_catch[tree.tree_id] = cls(tree) + return cls._tree_catch[tree.tree_id] + + def walk(self, outdated: list[Node]) -> tuple[Node, list[NodeSocket]]: + if self.__walk_catch is None: + outdated_nodes = set(self._bfs_walk(list(outdated))) + from_outdated: dict[Node, set[Node]] = defaultdict(set) + for n in outdated_nodes: + if n in self._from_nodes: + from_outdated[n] = {_n for _n in self._from_nodes[n] if _n in outdated_nodes} + self.__walk_catch = [] + for node in TopologicalSorter(from_outdated).static_order(): + self.__walk_catch.append((node, [self._from_sock.get(s) for s in node.inputs])) + + for node, other_socks in self.__walk_catch: + if all(n.get(UPDATE_KEY, True) for sock in other_socks if (n := self._sock_node.get(sock))): + yield node, other_socks + else: + node[UPDATE_KEY] = False + + @classmethod + def reset_tree(cls, tree: NodeTree = None): + """It can be called when the tree changed its topology with the tree + parameter or when Undo event has happened without arguments""" + if tree is not None and tree.tree_id in cls._tree_catch: + del cls._tree_catch[tree.tree_id] + else: + cls._tree_catch.clear() + + def reset_walk(self): + """It should be called when some animation properties of the tree + nodes were changed""" + self.__walk_catch = None + + def _remove_reroutes(self): + for _node in self._from_nodes: + if _node.bl_idname == "NodeReroute": + + # relink nodes + from_n = self._from_nodes[_node].pop() + self._to_nodes[from_n].remove(_node) # remove from + to_ns = self._to_nodes[_node] + for _next in to_ns: + self._from_nodes[_next].remove(_node) # remove to + self._from_nodes[_next].add(from_n) # add link from + self._to_nodes[from_n].add(_next) # add link to + + # relink sockets + for sock in _next.inputs: + from_s = self._from_sock.get(sock) + if from_s is None: + continue + from_s_node = self._sock_node[from_s] + if from_s_node == _node: + from_from_s = self._from_sock.get(from_s_node.inputs[0]) + if from_from_s is not None: + self._from_sock[sock] = from_from_s + else: + del self._from_sock[sock] + + self._from_nodes = {n: k for n, k in self._from_nodes.items() if n.bl_idname != 'NodeReroute'} + self._to_nodes = {n: k for n, k in self._to_nodes.items() if n.bl_idname != 'NodeReroute'} + + def _bfs_walk(self, nodes: list[Node]) -> Generator[Node, None, None]: + """ + Walk from the current node, it will visit all next nodes + First will be visited children nodes than children of children nodes etc. + https://en.wikipedia.org/wiki/Breadth-first_search + """ + + def node_walker(_node: Node): + for nn in self._to_nodes.get(_node, []): + yield nn + + waiting_nodes = deque(nodes) + discovered = set(nodes) + + for i in range(20000): + if not waiting_nodes: + break + n = waiting_nodes.popleft() + yield n + for next_node in node_walker(n): + if next_node not in discovered: + waiting_nodes.append(next_node) + discovered.add(next_node) + else: + raise RecursionError(f'The tree has either more then={20000} nodes ' + f'or most likely it is circular') diff --git a/core/sockets.py b/core/sockets.py index f3ac85e625..fc65224733 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -337,6 +337,7 @@ class SvSocketCommon(SvSocketProcessing): nesting_level: IntProperty(default=2) default_mode: EnumProperty(items=enum_item_4(['NONE', 'EMPTY_LIST', 'MATRIX', 'MASK']), default='EMPTY_LIST') pre_processing: EnumProperty(items=enum_item_4(['NONE', 'ONE_ITEM']), default='NONE') + s_id: StringProperty(options={'SKIP_SAVE'}) def get_link_parameter_node(self): return self.quick_link_to_node @@ -369,7 +370,11 @@ def other(self): @property def socket_id(self): """Id of socket used by data_cache""" - return str(hash(self.node.node_id + self.identifier + ('o' if self.is_output else 'i'))) + _id = self.s_id + if not _id: + self.s_id = str(hash(self.node.node_id + self.identifier + ('o' if self.is_output else 'i'))) + _id = self.s_id + return _id @property def index(self): diff --git a/node_tree.py b/node_tree.py index edadd7dfb4..e6cdcb4d3c 100644 --- a/node_tree.py +++ b/node_tree.py @@ -169,7 +169,7 @@ def nodes_to_update(): if self.sv_scene_update: TreeHandler.send(TreeEvent(TreeEvent.SCENE_UPDATE, self, nodes_to_update(), cancel=False)) - def process_ani(self): + def process_ani(self, frame_changed: bool, animation_playing: bool): """ Process the Sverchok node tree if animation layers show true. For animation callback/handler @@ -182,18 +182,18 @@ def animated_nodes(): except AttributeError: pass if self.sv_animate: - TreeHandler.send(TreeEvent(TreeEvent.FRAME_CHANGE, self, animated_nodes())) - - def update_ui(self): + TreeHandler.send(TreeEvent( + TreeEvent.FRAME_CHANGE, + self, + animated_nodes(), + is_frame_changed=frame_changed, + is_animation_playing=animation_playing)) + + def update_ui(self, nodes_errors, update_time): """ The method get information about node statistic of last update from the handler to show in view space The method is usually called by main handler to reevaluate view of the nodes in the tree even if the tree is not in the Live update mode""" - nodes_errors = TreeHandler.get_error_nodes(self) - if self.sv_show_time_nodes: - update_time = (TreeHandler.get_cum_time(self) if self.show_time_mode == "Cumulative" - else TreeHandler.get_update_time(self)) - else: - update_time = cycle([None]) + update_time = update_time if self.sv_show_time_nodes else cycle([None]) for node, error, update in zip(self.nodes, nodes_errors, update_time): if hasattr(node, 'update_ui'): node.update_ui(error, update) @@ -227,7 +227,10 @@ def refresh_node(self, context): self.process_node(context) refresh: BoolProperty(name="Update Node", description="Update Node", update=refresh_node) - is_animatable: BoolProperty(name="Animate Node", description="Update Node on frame change", default=True) + is_animatable: BoolProperty(name="Animate Node", + description="Update Node on frame change", + default=True, + update=lambda s, c: s.process_node(c)) # it would be better to have special event is_animation_dependent = False # if True and is_animatable the the node will be updated on frame change def sv_init(self, context): diff --git a/nodes/list_struct/item.py b/nodes/list_struct/item.py index a6e57d27d6..e69aead245 100644 --- a/nodes/list_struct/item.py +++ b/nodes/list_struct/item.py @@ -74,14 +74,14 @@ def process(self): if out_item.is_linked: if self.level-1: - out = self.get(data, self.level-1, indexes, self.get_items) + out = self.get_(data, self.level-1, indexes, self.get_items) else: out = self.get_items(data, indexes[0]) out_item.sv_set(out) if out_other.is_linked: if self.level-1: - out = self.get(data, self.level-1, indexes, self.get_other) + out = self.get_(data, self.level-1, indexes, self.get_other) else: out = self.get_other(data, indexes[0]) out_other.sv_set(out) @@ -125,13 +125,13 @@ def get_other(self, data, indexes): else: return None - def get(self, data, level, indexes, func): + def get_(self, data, level, indexes, func): # get is build-in method of Node class '''iterative function to get down to the requested level''' if level == 1: index_iter = repeat_last(indexes) - return [self.get(obj, level-1, next(index_iter), func) for obj in data] + return [self.get_(obj, level-1, next(index_iter), func) for obj in data] elif level: - return [self.get(obj, level-1, indexes, func) for obj in data] + return [self.get_(obj, level-1, indexes, func) for obj in data] else: return func(data, indexes) diff --git a/nodes/list_struct/split.py b/nodes/list_struct/split.py index 70e0975bb0..55f5815e16 100644 --- a/nodes/list_struct/split.py +++ b/nodes/list_struct/split.py @@ -73,20 +73,20 @@ def process(self): data = self.inputs['Data'].sv_get(deepcopy=False) sizes = self.inputs['Split'].sv_get(deepcopy=False)[0] if self.unwrap: - out = self.get(data, self.level_unwrap, sizes) + out = self.get_(data, self.level_unwrap, sizes) elif self.level: - out = self.get(data, self.level, sizes) + out = self.get_(data, self.level, sizes) else: out = split(data, sizes[0]) self.outputs['Split'].sv_set(out) - def get(self, data, level, size): + def get_(self, data, level, size): # get is buid-in method for nodes if not isinstance(data, (list, tuple)): return data if not isinstance(data[0], (list, tuple, np.ndarray, str)): return data if level > 1: # find level to work on - return [self.get(d, level - 1, size) for d in data] + return [self.get_(d, level - 1, size) for d in data] elif level == 1: # execute the chosen function sizes = repeat_last(size) if self.unwrap: From af65e997982922e43ea06db07db34fd9211cc052 Mon Sep 17 00:00:00 2001 From: Durman Date: Sat, 14 May 2022 23:31:44 +0400 Subject: [PATCH 06/25] convert NodesUpdater to Task class --- core/group_handlers.py | 9 +-- core/main_tree_handler.py | 134 ++++++++++++++++---------------------- ui/nodeview_keymaps.py | 4 +- 3 files changed, 63 insertions(+), 84 deletions(-) diff --git a/core/group_handlers.py b/core/group_handlers.py index 9c1733cded..3961463604 100644 --- a/core/group_handlers.py +++ b/core/group_handlers.py @@ -16,7 +16,7 @@ from typing import Generator, TYPE_CHECKING, Union, List, Optional, Iterator, Tuple from sverchok.core.events import GroupEvent -from sverchok.core.main_tree_handler import empty_updater, NodesUpdater, ContextTrees, handle_node_data, PathManager +from sverchok.core.main_tree_handler import empty_updater, Task, ContextTrees, handle_node_data, PathManager from sverchok.core.sv_custom_exceptions import CancelError from sverchok.utils.tree_structure import Node from sverchok.utils.logging import log_error @@ -42,9 +42,10 @@ def update(cls, event: GroupEvent, trees_ui_to_update: set) -> Iterator[Node]: @classmethod def send(cls, event: GroupEvent): + current_task = Task.get() # this should be first other wise other instructions can spoil the node statistic to redraw - if NodesUpdater.is_running(): - NodesUpdater.cancel_task() + if current_task and current_task.is_running(): + current_task.cancel() # mark given nodes as outdated if event.type == GroupEvent.NODES_UPDATE: @@ -67,7 +68,7 @@ def send(cls, event: GroupEvent): # Add update tusk for the tree if event.to_update: - NodesUpdater.add_task(event) + Task.add(event) @staticmethod def get_error_nodes(group_nodes_path: List[SvGroupTreeNode]) -> Iterator[Optional[Exception]]: diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index 9fef0f6a25..00e6c8dd6c 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -35,12 +35,14 @@ class TreeHandler: @staticmethod def send(event: TreeEvent): + """Control center""" # debug(event.type) + current_task = Task.get() # this should be first other wise other instructions can spoil the node statistic to redraw - if NodesUpdater.is_running(): + if current_task and current_task.is_running(): if event.cancel: - NodesUpdater.cancel_task() + current_task.cancel() else: return # ignore the event @@ -56,7 +58,7 @@ def send(event: TreeEvent): # something changed in scene and it duplicates some tree events which should be ignored elif event.type == TreeEvent.SCENE_UPDATE: # Either the scene handler was triggered by changes in the tree or tree is still in progress - if NodesUpdater.has_task(): + if current_task: return # ignore the event # this event was caused my update system itself and should be ignored elif 'SKIP_UPDATE' in event.tree: @@ -89,7 +91,7 @@ def send(event: TreeEvent): raise TypeError(f'Detected unknown event - {event}') # Add update tusk for the tree - NodesUpdater.add_task(event) + Task.add(event) @staticmethod def get_error_nodes(bl_tree) -> Iterator[Optional[Exception]]: @@ -125,127 +127,103 @@ def get_cum_time(bl_tree) -> Iterator[Optional[float]]: def tree_event_loop(delay): """Sverchok event handler""" with catch_log_error(): - if NodesUpdater.is_running(): - NodesUpdater.run_task() - elif NodesUpdater.has_task(): # task should be run via timer only https://developer.blender.org/T82318#1053877 - NodesUpdater.start_task() - NodesUpdater.run_task() + if task := Task.get(): + if not task.is_running(): + task.start() + task.run() # task should be run via timer only https://developer.blender.org/T82318#1053877 return delay tree_event_loop = partial(tree_event_loop, 0.01) -class NodesUpdater: - """It can update only one tree at a time""" - _event: Union[TreeEvent, GroupEvent] = None - _handler: Optional[Generator] = None +class Task: + _task: Optional['Task'] = None # for now running only one task is supported - _node_tree_area: Optional[bpy.types.Area] = None - _last_node: Optional[Node] = None - - _start_time: float = None + __slots__ = ('_event', '_handler', '_node_tree_area', '_start_time', '_last_node') @classmethod - def add_task(cls, event: Union[TreeEvent, GroupEvent]): - """It can handle only one tree at a time""" - if cls.is_running(): - raise RuntimeError(f"Can't update tree: {event.tree.name}, already updating tree: {cls._event.tree.name}") - cls._event = event + def add(cls, event: TreeEvent) -> 'Task': + if cls._task and cls._task.is_running(): + raise RuntimeError(f"Can't update tree: {event.tree.name}," + f" already updating tree: {cls._task._event.tree.name}") + cls._task = cls(event) + return cls._task @classmethod - def start_task(cls): - changed_tree = cls._event.tree - if cls.is_running(): + def get(cls) -> Optional['Task']: + return cls._task + + def __init__(self, event): + self._event: TreeEvent = event + self._handler: Optional[Generator] = None + self._node_tree_area: Optional[bpy.types.Area] = None + self._start_time: Optional[float] = None + self._last_node: Optional[Node] = None + + def start(self): + changed_tree = self._event.tree + if self.is_running(): raise RuntimeError(f'Tree "{changed_tree.name}" already is being updated') - cls._handler = global_updater(cls._event.type) + self._handler = global_updater(self._event.type) # searching appropriate area index for reporting update progress for area in bpy.context.screen.areas: if area.ui_type == 'SverchCustomTreeType': path = area.spaces[0].path if path and path[-1].node_tree.name == changed_tree.name: - cls._node_tree_area = area + self._node_tree_area = area break gc.disable() - cls._start_time = time() + self._start_time = time() - @classmethod @profile(section="UPDATE") - def run_task(cls): + def run(self): try: # handle un-cancellable events - if cls._event.type == TreeEvent.FRAME_CHANGE: + if self._event.type == TreeEvent.FRAME_CHANGE: while True: - next(cls._handler) + next(self._handler) # handler cancellable events else: - if cls._last_node: - cls._last_node.bl_tween.set_temp_color() + if self._last_node: + self._last_node.bl_tween.set_temp_color() start_time = time() while (time() - start_time) < 0.15: # 0.15 is max timer frequency - node = next(cls._handler) + node = next(self._handler) - cls._last_node = node + self._last_node = node node.bl_tween.set_temp_color((0.7, 1.000000, 0.7)) - cls._report_progress(f'Pres "ESC" to abort, updating node "{node.name}"') - - except StopIteration: - cls.finish_task() - - @classmethod - def debug_run_task(cls): - """Color updated nodes for a few second after all""" - try: - start_time = time() - while (time() - start_time) < 0.15: # 0.15 is max timer frequency - node = next(cls._handler) - if node is not None: - node.bl_tween.set_temp_color((0.7, 1.000000, 0.7)) - else: - return - - cls._last_node = node - cls._report_progress(f'Pres "ESC" to abort, updating node "{node.name}"') + self._report_progress(f'Pres "ESC" to abort, updating node "{node.name}"') except StopIteration: - from time import sleep - sleep(1) - cls.finish_task() + self.finish_task() - @classmethod - def cancel_task(cls): + def cancel(self): try: - cls._handler.throw(CancelError) + self._handler.throw(CancelError) except (StopIteration, RuntimeError): pass finally: # protection from the task to be stack forever - cls.finish_task() + self.finish_task() - @classmethod - def finish_task(cls): + def finish_task(self): try: gc.enable() - debug(f'Global update - {int((time() - cls._start_time) * 1000)}ms') - cls._report_progress() + debug(f'Global update - {int((time() - self._start_time) * 1000)}ms') + self._report_progress() finally: - cls._event, cls._handler, cls._node_tree_area, cls._last_node, cls._start_time = [None] * 5 + Task._task = None - @classmethod - def has_task(cls) -> bool: - return cls._event is not None - - @classmethod - def is_running(cls) -> bool: - return cls._handler is not None + def is_running(self) -> bool: + return self._handler is not None - @classmethod - def _report_progress(cls, text: str = None): - if cls._node_tree_area: - cls._node_tree_area.header_text_set(text) + def _report_progress(self, text: str = None): + if self._node_tree_area: + self._node_tree_area.header_text_set(text) def global_updater(event_type: str) -> Generator[Node, None, None]: diff --git a/ui/nodeview_keymaps.py b/ui/nodeview_keymaps.py index 105ec52042..6e3a4d97d0 100644 --- a/ui/nodeview_keymaps.py +++ b/ui/nodeview_keymaps.py @@ -109,8 +109,8 @@ class PressingEscape(bpy.types.Operator): bl_label = 'Abort nodes updating' def execute(self, context): - if main_tree_handler.NodesUpdater.is_running(): - main_tree_handler.NodesUpdater.cancel_task() + if (task := main_tree_handler.Task.get()) and task.is_running(): + task.cancel() return {'FINISHED'} @classmethod From 85fa2a31eabbdbba0caa070cf678fb566dd11d5d Mon Sep 17 00:00:00 2001 From: Durman Date: Tue, 17 May 2022 19:17:37 +0400 Subject: [PATCH 07/25] add more general solution for walk catching add event manager function --- core/main_tree_handler.py | 76 +++++++----- core/simple_update_system.py | 229 +++++++++++++++++++++-------------- utils/tree_walk.py | 27 ++++- 3 files changed, 208 insertions(+), 124 deletions(-) diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index 00e6c8dd6c..74702b94cf 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -46,15 +46,6 @@ def send(event: TreeEvent): else: return # ignore the event - # frame update - # This event can't be handled via NodesUpdater during animation rendering because new frame change event - # can arrive before timer finishes its tusk. Or timer can start working before frame change is handled. - if event.type == TreeEvent.FRAME_CHANGE: - sus.process_nodes(event) - # ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - # profile(section="UPDATE")(lambda: list(global_updater(event.type)))() - return - # something changed in scene and it duplicates some tree events which should be ignored elif event.type == TreeEvent.SCENE_UPDATE: # Either the scene handler was triggered by changes in the tree or tree is still in progress @@ -64,34 +55,14 @@ def send(event: TreeEvent): elif 'SKIP_UPDATE' in event.tree: del event.tree['SKIP_UPDATE'] return - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - - # mark given nodes as outdated - elif event.type == TreeEvent.NODES_UPDATE: - sus.Tree.get(event.tree).reset_walk() - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - - # it will find changes in tree topology and mark related nodes as outdated - elif event.type == TreeEvent.TREE_UPDATE: - sus.Tree.reset_tree(event.tree) - ContextTrees.mark_tree_outdated(event.tree) # force update elif event.type == TreeEvent.FORCE_UPDATE: - ContextTrees.reset_data(event.tree) event.tree['FORCE_UPDATE'] = True - # new file opened - elif event.type == TreeEvent.FILE_RELOADED: - sus.Tree.reset_tree() - ContextTrees.reset_data() - - # Unknown event - else: - raise TypeError(f'Detected unknown event - {event}') - # Add update tusk for the tree - Task.add(event) + if sus.control_center(event): + Task.add(event) @staticmethod def get_error_nodes(bl_tree) -> Iterator[Optional[Exception]]: @@ -124,6 +95,44 @@ def get_cum_time(bl_tree) -> Iterator[Optional[float]]: yield cum_time_nodes.get(node) +def control_center(event: TreeEvent) -> bool: + add_tusk = True + + # something changed in scene and it duplicates some tree events which should be ignored + if event.type == TreeEvent.SCENE_UPDATE: + ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) + + # frame update + # This event can't be handled via NodesUpdater during animation rendering because new frame change event + # can arrive before timer finishes its tusk. Or timer can start working before frame change is handled. + elif event.type == TreeEvent.FRAME_CHANGE: + ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) + profile(section="UPDATE")(lambda: list(global_updater(event.type)))() + add_tusk = False + + # mark given nodes as outdated + elif event.type == TreeEvent.NODES_UPDATE: + ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) + + # it will find changes in tree topology and mark related nodes as outdated + elif event.type == TreeEvent.TREE_UPDATE: + ContextTrees.mark_tree_outdated(event.tree) + + # force update + elif event.type == TreeEvent.FORCE_UPDATE: + ContextTrees.reset_data(event.tree) + + # new file opened + elif event.type == TreeEvent.FILE_RELOADED: + ContextTrees.reset_data() + + # Unknown event + else: + raise TypeError(f'Detected unknown event - {event}') + + return add_tusk + + def tree_event_loop(delay): """Sverchok event handler""" with catch_log_error(): @@ -272,8 +281,9 @@ def global_updater(event_type: str) -> Generator[Node, None, None]: # this will update all opened trees (in group editors) # regardless whether the trees was changed or not, including group nodes for bl_tree in trees_ui_to_update: - args = [bl_tree.get_update_path()] if BlTree(bl_tree).is_group_tree else [] - bl_tree.update_ui(*args) + update_ui(bl_tree) + # args = [bl_tree.get_update_path()] if BlTree(bl_tree).is_group_tree else [] + # bl_tree.update_ui(*args) def update_ui(tree): diff --git a/core/simple_update_system.py b/core/simple_update_system.py index 8f3fca14b3..354a90f2be 100644 --- a/core/simple_update_system.py +++ b/core/simple_update_system.py @@ -1,59 +1,65 @@ -from collections import defaultdict, deque +from collections import defaultdict +from contextlib import contextmanager +from functools import lru_cache from graphlib import TopologicalSorter from time import perf_counter -from typing import Optional, Generator +from typing import TYPE_CHECKING, TypeVar, Literal, Optional from bpy_types import Node, NodeSocket, NodeTree from sverchok.core.events import TreeEvent from sverchok.core.socket_conversions import ConversionPolicies from sverchok.utils.profile import profile from sverchok.utils.logging import log_error +from sverchok.utils.tree_walk import bfs_walk + +if TYPE_CHECKING: + from sverchok.node_tree import SverchCustomTreeNode as SvNode UPDATE_KEY = "US_is_updated" ERROR_KEY = "US_error" TIME_KEY = "US_time" +T = TypeVar('T') -@profile(section="UPDATE") -def process_nodes(event: TreeEvent): - if event.is_frame_changed: - for node, prev_socks in Tree.get(event.tree).walk(event.updated_nodes): - # node.set_temp_color([1, 1, 1]) # execution debug - try: - t = perf_counter() - prepare_input_data(prev_socks, node.inputs) - node.process() - node[UPDATE_KEY] = True - node[ERROR_KEY] = None - node[TIME_KEY] = perf_counter() - t - except Exception as e: - log_error(e) - node[UPDATE_KEY] = False - node[ERROR_KEY] = repr(e) - if not event.is_animation_playing: - update_ui(event.tree) +def control_center(event: TreeEvent) -> bool: + add_tusk = True + # frame update + # This event can't be handled via NodesUpdater during animation rendering because new frame change event + # can arrive before timer finishes its tusk. Or timer can start working before frame change is handled. + if event.type == TreeEvent.FRAME_CHANGE: + Tree.get(event.tree).update(event) + add_tusk = False -def prepare_input_data(prev_socks, input_socks): - for ps, ns in zip(prev_socks, input_socks): - if ps is None: - continue - data = ps.sv_get() + # something changed in scene and it duplicates some tree events which should be ignored + elif event.type == TreeEvent.SCENE_UPDATE: + pass # todo similar to animation + # ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - # cast data - if ps.bl_idname != ns.bl_idname: - implicit_conversion = ConversionPolicies.get_conversion(ns.default_conversion_name) - data = implicit_conversion.convert(ns, ps, data) + # mark given nodes as outdated + elif event.type == TreeEvent.NODES_UPDATE: + pass # todo add to outdated_nodes? - ns.sv_set(data) + # it will find changes in tree topology and mark related nodes as outdated + elif event.type == TreeEvent.TREE_UPDATE: + Tree.reset_tree(event.tree) # todo should detect difference + # force update + elif event.type == TreeEvent.FORCE_UPDATE: + Tree.reset_tree(event.tree) -def update_ui(tree: NodeTree): - errors = (n.get(ERROR_KEY, None) for n in tree.nodes) - times = (n.get(TIME_KEY, 0) for n in tree.nodes) - tree.update_ui(errors, times) + # new file opened + elif event.type == TreeEvent.FILE_RELOADED: + Tree.reset_tree() + + # Unknown event + else: + raise TypeError(f'Detected unknown event - {event}') + + # Add update tusk for the tree + return add_tusk class Tree: @@ -61,12 +67,44 @@ class Tree: tree data structure""" _tree_catch: dict[str, 'Tree'] = dict() # the module should be auto-reloaded to prevent crashes + WALK_MODE = Literal['animation', 'all'] + + @classmethod + def get(cls, tree: NodeTree): + if tree.tree_id not in cls._tree_catch: + _tree = cls(tree) + cls._tree_catch[tree.tree_id] = _tree + return cls._tree_catch[tree.tree_id] + + @profile(section="UPDATE") + def update(self, event: TreeEvent): + if event.is_frame_changed: + tree = Tree.get(event.tree) + for node, prev_socks in tree._walk(list(event.updated_nodes)): + # node.set_temp_color([1, 1, 1]) # execution debug + with add_statistic(node): + prepare_input_data(prev_socks, node.inputs) + node.process() + + if not event.is_animation_playing: + update_ui(event.tree) + + @classmethod + def reset_tree(cls, tree: NodeTree = None): + """It can be called when the tree changed its topology with the tree + parameter or when Undo event has happened without arguments""" + if tree is not None and tree.tree_id in cls._tree_catch: + del cls._tree_catch[tree.tree_id] + else: + cls._tree_catch.clear() + def __init__(self, tree: NodeTree): - self._from_nodes: dict[Node, set[Node]] = defaultdict(set) - self._to_nodes: dict[Node, set[Node]] = defaultdict(set) + self._tree = tree + self._from_nodes: dict[SvNode, set[SvNode]] = defaultdict(set) + self._to_nodes: dict[SvNode, set[SvNode]] = defaultdict(set) self._from_sock: dict[NodeSocket, NodeSocket] = dict() self._sock_node: dict[NodeSocket, Node] = dict() - self.__walk_catch: Optional[tuple[Node, list[NodeSocket]]] = None + self._outdated_nodes: Optional[list[SvNode]] = None # None means outdated all for link in (li for li in tree.links if not li.is_muted): self._from_nodes[link.to_node].add(link.from_node) @@ -76,42 +114,46 @@ def __init__(self, tree: NodeTree): self._remove_reroutes() - @classmethod - def get(cls, tree: NodeTree): - if tree.tree_id not in cls._tree_catch: - cls._tree_catch[tree.tree_id] = cls(tree) - return cls._tree_catch[tree.tree_id] - - def walk(self, outdated: list[Node]) -> tuple[Node, list[NodeSocket]]: - if self.__walk_catch is None: - outdated_nodes = set(self._bfs_walk(list(outdated))) - from_outdated: dict[Node, set[Node]] = defaultdict(set) - for n in outdated_nodes: - if n in self._from_nodes: - from_outdated[n] = {_n for _n in self._from_nodes[n] if _n in outdated_nodes} - self.__walk_catch = [] - for node in TopologicalSorter(from_outdated).static_order(): - self.__walk_catch.append((node, [self._from_sock.get(s) for s in node.inputs])) + def _walk(self, outdated: list['SvNode'] = None) -> tuple[Node, list[NodeSocket]]: + # walk all nodes in the tree + if self._outdated_nodes is None: + outdated = None + self._outdated_nodes = [] + # walk triggered nodes and error nodes from previous updates + else: + outdated.extend(self._outdated_nodes) + outdated = frozenset(outdated) + self._outdated_nodes.clear() - for node, other_socks in self.__walk_catch: + for node, other_socks in self._sort_nodes(outdated): + # execute node only if all previous nodes are updated if all(n.get(UPDATE_KEY, True) for sock in other_socks if (n := self._sock_node.get(sock))): yield node, other_socks + if node.get(ERROR_KEY, False): + self._outdated_nodes.append(node) else: node[UPDATE_KEY] = False - @classmethod - def reset_tree(cls, tree: NodeTree = None): - """It can be called when the tree changed its topology with the tree - parameter or when Undo event has happened without arguments""" - if tree is not None and tree.tree_id in cls._tree_catch: - del cls._tree_catch[tree.tree_id] - else: - cls._tree_catch.clear() + @lru_cache(maxsize=1) + def _sort_nodes(self, outdated_nodes: frozenset['SvNode'] = None) -> list[tuple['SvNode', list[NodeSocket]]]: - def reset_walk(self): - """It should be called when some animation properties of the tree - nodes were changed""" - self.__walk_catch = None + def node_walker(node_: 'SvNode'): + for nn in self._to_nodes.get(node_, []): + yield nn + + nodes = [] + if outdated_nodes is None: + for node in TopologicalSorter(self._from_nodes).static_order(): + nodes.append((node, [self._from_sock.get(s) for s in node.inputs])) + else: + outdated_nodes = set(bfs_walk(outdated_nodes, node_walker)) + from_outdated: dict[SvNode, set[SvNode]] = defaultdict(set) + for n in outdated_nodes: + if n in self._from_nodes: + from_outdated[n] = {_n for _n in self._from_nodes[n] if _n in outdated_nodes} + for node in TopologicalSorter(from_outdated).static_order(): + nodes.append((node, [self._from_sock.get(s) for s in node.inputs])) + return nodes def _remove_reroutes(self): for _node in self._from_nodes: @@ -142,29 +184,36 @@ def _remove_reroutes(self): self._from_nodes = {n: k for n, k in self._from_nodes.items() if n.bl_idname != 'NodeReroute'} self._to_nodes = {n: k for n, k in self._to_nodes.items() if n.bl_idname != 'NodeReroute'} - def _bfs_walk(self, nodes: list[Node]) -> Generator[Node, None, None]: - """ - Walk from the current node, it will visit all next nodes - First will be visited children nodes than children of children nodes etc. - https://en.wikipedia.org/wiki/Breadth-first_search - """ - def node_walker(_node: Node): - for nn in self._to_nodes.get(_node, []): - yield nn +@contextmanager +def add_statistic(node): + try: + t = perf_counter() + yield None + node[UPDATE_KEY] = True + node[ERROR_KEY] = None + node[TIME_KEY] = perf_counter() - t + except Exception as e: + log_error(e) + node[UPDATE_KEY] = False + node[ERROR_KEY] = repr(e) - waiting_nodes = deque(nodes) - discovered = set(nodes) - - for i in range(20000): - if not waiting_nodes: - break - n = waiting_nodes.popleft() - yield n - for next_node in node_walker(n): - if next_node not in discovered: - waiting_nodes.append(next_node) - discovered.add(next_node) - else: - raise RecursionError(f'The tree has either more then={20000} nodes ' - f'or most likely it is circular') + +def prepare_input_data(prev_socks, input_socks): + for ps, ns in zip(prev_socks, input_socks): + if ps is None: + continue + data = ps.sv_get() + + # cast data + if ps.bl_idname != ns.bl_idname: + implicit_conversion = ConversionPolicies.get_conversion(ns.default_conversion_name) + data = implicit_conversion.convert(ns, ps, data) + + ns.sv_set(data) + + +def update_ui(tree: NodeTree): + errors = (n.get(ERROR_KEY, None) for n in tree.nodes) + times = (n.get(TIME_KEY, 0) for n in tree.nodes) + tree.update_ui(errors, times) \ No newline at end of file diff --git a/utils/tree_walk.py b/utils/tree_walk.py index 45764618d7..d34639e867 100644 --- a/utils/tree_walk.py +++ b/utils/tree_walk.py @@ -11,7 +11,32 @@ from abc import ABC, abstractmethod from collections import deque from itertools import count -from typing import Generator, List, TypeVar, Generic +from typing import Generator, List, TypeVar, Generic, Callable, Iterable + +T = TypeVar('T') + + +def bfs_walk(nodes: Iterable[T], next_: Callable[[T], Iterable[T]]) -> Generator[T, None, None]: + """ + Walk from the current node, it will visit all next nodes + First will be visited children nodes than children of children nodes etc. + https://en.wikipedia.org/wiki/Breadth-first_search + """ + waiting_nodes = deque(nodes) + discovered = set(nodes) + + for i in range(20000): + if not waiting_nodes: + break + n = waiting_nodes.popleft() + yield n + for next_node in next_(n): + if next_node not in discovered: + waiting_nodes.append(next_node) + discovered.add(next_node) + else: + raise RecursionError(f'The tree has either more then={20000} nodes ' + f'or most likely it is circular') class Node(ABC): From 86cf19c8495d133df480db71c11cdad6bb1752df Mon Sep 17 00:00:00 2001 From: Durman Date: Tue, 17 May 2022 22:19:27 +0400 Subject: [PATCH 08/25] convert update function into a generator optimize the context manager --- core/main_tree_handler.py | 60 +++++++++++++++--------------- core/simple_update_system.py | 71 ++++++++++++++++++++++-------------- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index 74702b94cf..f5e7c44184 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -11,21 +11,21 @@ from contextlib import contextmanager from functools import partial from time import time -from typing import Dict, Generator, Optional, Iterator, Tuple, Union, NewType, List, TYPE_CHECKING +from typing import Dict, Generator, Optional, Iterator, Tuple, NewType, List, TYPE_CHECKING, Callable import bpy from sverchok.core.sv_custom_exceptions import SvNoDataError, CancelError from sverchok.core.socket_conversions import ConversionPolicies from sverchok.data_structure import post_load_call -from sverchok.core.events import TreeEvent, GroupEvent +from sverchok.core.events import TreeEvent from sverchok.utils.logging import debug, catch_log_error, log_error from sverchok.utils.tree_structure import Tree, Node -from sverchok.utils.handle_blender_data import BlTrees, BlTree, BlNode +from sverchok.utils.handle_blender_data import BlTrees, BlNode from sverchok.utils.profile import profile import sverchok.core.simple_update_system as sus if TYPE_CHECKING: - from sverchok.core.node_group import SvGroupTreeNode + from sverchok.core.node_group import SvGroupTreeNode as SvNode Path = NewType('Path', str) # concatenation of group node ids @@ -61,8 +61,8 @@ def send(event: TreeEvent): event.tree['FORCE_UPDATE'] = True # Add update tusk for the tree - if sus.control_center(event): - Task.add(event) + if handler := sus.control_center(event): + Task.add(event, handler) @staticmethod def get_error_nodes(bl_tree) -> Iterator[Optional[Exception]]: @@ -149,32 +149,39 @@ def tree_event_loop(delay): class Task: _task: Optional['Task'] = None # for now running only one task is supported - __slots__ = ('_event', '_handler', '_node_tree_area', '_start_time', '_last_node') + __slots__ = ('_event', + '_handler_func', + '_handler', + '_node_tree_area', + '_start_time', + '_last_node', + ) @classmethod - def add(cls, event: TreeEvent) -> 'Task': + def add(cls, event: TreeEvent, handler: Callable) -> 'Task': if cls._task and cls._task.is_running(): raise RuntimeError(f"Can't update tree: {event.tree.name}," f" already updating tree: {cls._task._event.tree.name}") - cls._task = cls(event) + cls._task = cls(event, handler) return cls._task @classmethod def get(cls) -> Optional['Task']: return cls._task - def __init__(self, event): + def __init__(self, event, handler): self._event: TreeEvent = event - self._handler: Optional[Generator] = None + self._handler_func: Callable[[TreeEvent], Generator] = handler + self._handler: Optional[Generator[SvNode, None, None]] = None self._node_tree_area: Optional[bpy.types.Area] = None self._start_time: Optional[float] = None - self._last_node: Optional[Node] = None + self._last_node: Optional[SvNode] = None def start(self): changed_tree = self._event.tree if self.is_running(): raise RuntimeError(f'Tree "{changed_tree.name}" already is being updated') - self._handler = global_updater(self._event.type) + self._handler = self._handler_func(self._event) # searching appropriate area index for reporting update progress for area in bpy.context.screen.areas: @@ -190,23 +197,16 @@ def start(self): @profile(section="UPDATE") def run(self): try: - # handle un-cancellable events - if self._event.type == TreeEvent.FRAME_CHANGE: - while True: - next(self._handler) + if self._last_node: + self._last_node.set_temp_color() - # handler cancellable events - else: - if self._last_node: - self._last_node.bl_tween.set_temp_color() - - start_time = time() - while (time() - start_time) < 0.15: # 0.15 is max timer frequency - node = next(self._handler) + start_time = time() + while (time() - start_time) < 0.15: # 0.15 is max timer frequency + node = next(self._handler) - self._last_node = node - node.bl_tween.set_temp_color((0.7, 1.000000, 0.7)) - self._report_progress(f'Pres "ESC" to abort, updating node "{node.name}"') + self._last_node = node + node.set_temp_color((0.7, 1.000000, 0.7)) + self._report_progress(f'Pres "ESC" to abort, updating node "{node.name}"') except StopIteration: self.finish_task() @@ -421,7 +421,7 @@ def calc_cam_update_time(cls, bl_tree, context='') -> dict: return cum_time_nodes @classmethod - def calc_cam_update_time_group(cls, bl_tree, group_nodes: List[SvGroupTreeNode]) -> dict: + def calc_cam_update_time_group(cls, bl_tree, group_nodes: List[SvNode]) -> dict: cum_time_nodes = dict() if bl_tree.tree_id not in cls._trees: return cum_time_nodes @@ -472,7 +472,7 @@ def _update_topology_status(cls, new_tree: Tree): class PathManager: @staticmethod - def generate_path(group_nodes: List[SvGroupTreeNode]) -> Path: + def generate_path(group_nodes: List[SvNode]) -> Path: """path is ordered collection group node ids max length of path should be no more then number of base trees of most nested group node + 1""" return Path('.'.join(n.node_id for n in group_nodes)) diff --git a/core/simple_update_system.py b/core/simple_update_system.py index 354a90f2be..1d2e718fe7 100644 --- a/core/simple_update_system.py +++ b/core/simple_update_system.py @@ -1,9 +1,8 @@ from collections import defaultdict -from contextlib import contextmanager from functools import lru_cache from graphlib import TopologicalSorter from time import perf_counter -from typing import TYPE_CHECKING, TypeVar, Literal, Optional +from typing import TYPE_CHECKING, Literal, Optional, Generator, Callable from bpy_types import Node, NodeSocket, NodeTree from sverchok.core.events import TreeEvent @@ -20,18 +19,14 @@ ERROR_KEY = "US_error" TIME_KEY = "US_time" -T = TypeVar('T') - - -def control_center(event: TreeEvent) -> bool: - add_tusk = True +def control_center(event: TreeEvent) -> Optional[Callable[[TreeEvent], Generator]]: # frame update # This event can't be handled via NodesUpdater during animation rendering because new frame change event # can arrive before timer finishes its tusk. Or timer can start working before frame change is handled. if event.type == TreeEvent.FRAME_CHANGE: - Tree.get(event.tree).update(event) - add_tusk = False + Tree.update_animation(event) + return # something changed in scene and it duplicates some tree events which should be ignored elif event.type == TreeEvent.SCENE_UPDATE: @@ -40,7 +35,7 @@ def control_center(event: TreeEvent) -> bool: # mark given nodes as outdated elif event.type == TreeEvent.NODES_UPDATE: - pass # todo add to outdated_nodes? + pass # todo add to outdated_nodes? # it will find changes in tree topology and mark related nodes as outdated elif event.type == TreeEvent.TREE_UPDATE: @@ -59,7 +54,7 @@ def control_center(event: TreeEvent) -> bool: raise TypeError(f'Detected unknown event - {event}') # Add update tusk for the tree - return add_tusk + return Tree.update class Tree: @@ -76,13 +71,24 @@ def get(cls, tree: NodeTree): cls._tree_catch[tree.tree_id] = _tree return cls._tree_catch[tree.tree_id] + @classmethod @profile(section="UPDATE") - def update(self, event: TreeEvent): + def update_animation(cls, event: TreeEvent): + try: + g = cls.update(event) + while True: + next(g) + except StopIteration: + pass + + @classmethod + def update(cls, event: TreeEvent) -> Generator['SvNode', None, None]: if event.is_frame_changed: - tree = Tree.get(event.tree) - for node, prev_socks in tree._walk(list(event.updated_nodes)): + tree = cls.get(event.tree) + for node, prev_socks in tree._walk(list(event.updated_nodes or [])): # node.set_temp_color([1, 1, 1]) # execution debug - with add_statistic(node): + with AddStatistic(node): + yield node prepare_input_data(prev_socks, node.inputs) node.process() @@ -185,18 +191,27 @@ def _remove_reroutes(self): self._to_nodes = {n: k for n, k in self._to_nodes.items() if n.bl_idname != 'NodeReroute'} -@contextmanager -def add_statistic(node): - try: - t = perf_counter() - yield None - node[UPDATE_KEY] = True - node[ERROR_KEY] = None - node[TIME_KEY] = perf_counter() - t - except Exception as e: - log_error(e) - node[UPDATE_KEY] = False - node[ERROR_KEY] = repr(e) +class AddStatistic: + # using context manager from contextlib has big overhead + # https://stackoverflow.com/questions/26152934/why-the-staggering-overhead-50x-of-contextlib-and-the-with-statement-in-python + def __init__(self, node: 'SvNode'): + self._node = node + self._start = perf_counter() + + def __enter__(self): + return None + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is None: + self._node[UPDATE_KEY] = True + self._node[ERROR_KEY] = None + self._node[TIME_KEY] = perf_counter() - self._start + else: + log_error(exc_type) + self._node[UPDATE_KEY] = False + self._node[ERROR_KEY] = repr(exc_type) + + return isinstance(exc_type, Exception) def prepare_input_data(prev_socks, input_socks): @@ -216,4 +231,4 @@ def prepare_input_data(prev_socks, input_socks): def update_ui(tree: NodeTree): errors = (n.get(ERROR_KEY, None) for n in tree.nodes) times = (n.get(TIME_KEY, 0) for n in tree.nodes) - tree.update_ui(errors, times) \ No newline at end of file + tree.update_ui(errors, times) From d8e4dda0e33b6f3af620a56a404793827e446390 Mon Sep 17 00:00:00 2001 From: Durman Date: Thu, 19 May 2022 22:28:36 +0400 Subject: [PATCH 09/25] add support of editing trees --- core/main_tree_handler.py | 8 +++ core/simple_update_system.py | 99 ++++++++++++++++++++++++++++-------- 2 files changed, 87 insertions(+), 20 deletions(-) diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index f5e7c44184..4f4eb1553b 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -221,6 +221,14 @@ def cancel(self): def finish_task(self): try: + # this only need to trigger scene changes handler again + if self._event.tree.nodes: + status = self._event.tree.nodes[-1].use_custom_color + self._event.tree.nodes[-1].use_custom_color = not status + self._event.tree.nodes[-1].use_custom_color = status + # this indicates that process of the tree is finished and next scene event can be skipped + self._event.tree['SKIP_UPDATE'] = True + gc.enable() debug(f'Global update - {int((time() - self._start_time) * 1000)}ms') self._report_progress() diff --git a/core/simple_update_system.py b/core/simple_update_system.py index 1d2e718fe7..a66740dd99 100644 --- a/core/simple_update_system.py +++ b/core/simple_update_system.py @@ -2,9 +2,9 @@ from functools import lru_cache from graphlib import TopologicalSorter from time import perf_counter -from typing import TYPE_CHECKING, Literal, Optional, Generator, Callable +from typing import TYPE_CHECKING, Optional, Generator, Callable -from bpy_types import Node, NodeSocket, NodeTree +from bpy.types import Node, NodeSocket, NodeTree, NodeLink from sverchok.core.events import TreeEvent from sverchok.core.socket_conversions import ConversionPolicies from sverchok.utils.profile import profile @@ -39,7 +39,7 @@ def control_center(event: TreeEvent) -> Optional[Callable[[TreeEvent], Generator # it will find changes in tree topology and mark related nodes as outdated elif event.type == TreeEvent.TREE_UPDATE: - Tree.reset_tree(event.tree) # todo should detect difference + Tree.mark_outdated(event.tree) # force update elif event.type == TreeEvent.FORCE_UPDATE: @@ -48,6 +48,7 @@ def control_center(event: TreeEvent) -> Optional[Callable[[TreeEvent], Generator # new file opened elif event.type == TreeEvent.FILE_RELOADED: Tree.reset_tree() + return # Unknown event else: @@ -62,54 +63,68 @@ class Tree: tree data structure""" _tree_catch: dict[str, 'Tree'] = dict() # the module should be auto-reloaded to prevent crashes - WALK_MODE = Literal['animation', 'all'] - @classmethod - def get(cls, tree: NodeTree): + def get(cls, tree: NodeTree) -> 'Tree': if tree.tree_id not in cls._tree_catch: _tree = cls(tree) - cls._tree_catch[tree.tree_id] = _tree - return cls._tree_catch[tree.tree_id] + else: + _tree = cls._tree_catch[tree.tree_id] + if not _tree._is_updated: + old = _tree + _tree = cls(tree) + if old._outdated_nodes is not None: + _tree._outdated_nodes = old._outdated_nodes.copy() + _tree._outdated_nodes.extend(_tree._update_difference(old)) + return _tree @classmethod @profile(section="UPDATE") def update_animation(cls, event: TreeEvent): try: - g = cls.update(event) + g = cls.update(event, event.is_frame_changed, not event.is_animation_playing) while True: next(g) except StopIteration: pass @classmethod - def update(cls, event: TreeEvent) -> Generator['SvNode', None, None]: - if event.is_frame_changed: + def update(cls, event: TreeEvent, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: + if update_nodes: tree = cls.get(event.tree) - for node, prev_socks in tree._walk(list(event.updated_nodes or [])): - # node.set_temp_color([1, 1, 1]) # execution debug + walker = tree._walk(list(event.updated_nodes or [])) + # walker = tree._debug_color(walker) + for node, prev_socks in walker: with AddStatistic(node): yield node prepare_input_data(prev_socks, node.inputs) node.process() - if not event.is_animation_playing: + if update_interface: update_ui(event.tree) @classmethod def reset_tree(cls, tree: NodeTree = None): - """It can be called when the tree changed its topology with the tree - parameter or when Undo event has happened without arguments""" + """Remove tree data or data of all trees""" if tree is not None and tree.tree_id in cls._tree_catch: del cls._tree_catch[tree.tree_id] else: cls._tree_catch.clear() + @classmethod + def mark_outdated(cls, tree: NodeTree): + if _tree := cls._tree_catch.get(tree.tree_id): + _tree._is_updated = False + def __init__(self, tree: NodeTree): + self._tree_catch[tree.tree_id] = self self._tree = tree - self._from_nodes: dict[SvNode, set[SvNode]] = defaultdict(set) - self._to_nodes: dict[SvNode, set[SvNode]] = defaultdict(set) - self._from_sock: dict[NodeSocket, NodeSocket] = dict() - self._sock_node: dict[NodeSocket, Node] = dict() + self._from_nodes: dict[SvNode, set[SvNode]] = {n: set() for n in tree.nodes} + self._to_nodes: dict[SvNode, set[SvNode]] = {n: set() for n in tree.nodes} + self._from_sock: dict[NodeSocket, NodeSocket] = dict() # only connected + self._sock_node: dict[NodeSocket, Node] = dict() # only connected sockets + self._links: set[tuple[NodeSocket, NodeSocket]] = set() # from to socket + + self._is_updated = True # False if topology was changed self._outdated_nodes: Optional[list[SvNode]] = None # None means outdated all for link in (li for li in tree.links if not li.is_muted): @@ -117,6 +132,8 @@ def __init__(self, tree: NodeTree): self._to_nodes[link.from_node].add(link.to_node) self._from_sock[link.to_socket] = link.from_socket self._sock_node[link.from_socket] = link.from_node + self._sock_node[link.to_socket] = link.to_node + self._links.add((link.from_socket, link.to_socket)) self._remove_reroutes() @@ -161,6 +178,20 @@ def node_walker(node_: 'SvNode'): nodes.append((node, [self._from_sock.get(s) for s in node.inputs])) return nodes + def _update_difference(self, old: 'Tree') -> set['SvNode']: + nodes_to_update = self._from_nodes.keys() - old._from_nodes.keys() + new_links = self._links - old._links + for from_sock, to_sock in new_links: + if from_sock not in old._sock_node: # socket was not connected + # protect from if not self.outputs[0].is_linked: return + nodes_to_update.add(self._sock_node[from_sock]) + else: + nodes_to_update.add(self._sock_node[to_sock]) + removed_links = old._links - self._links + for from_sock, to_sock in removed_links: + nodes_to_update.add(old._sock_node[to_sock]) + return nodes_to_update + def _remove_reroutes(self): for _node in self._from_nodes: if _node.bl_idname == "NodeReroute": @@ -190,6 +221,34 @@ def _remove_reroutes(self): self._from_nodes = {n: k for n, k in self._from_nodes.items() if n.bl_idname != 'NodeReroute'} self._to_nodes = {n: k for n, k in self._to_nodes.items() if n.bl_idname != 'NodeReroute'} + def _debug_color(self, walker: Generator, use_color: bool = True): + def _set_color(node: 'SvNode', _use_color: bool): + use_key = "DEBUG_use_user_color" + color_key = "DEBUG_user_color" + + # set temporary color + if _use_color: + # save overridden color (only once) + if color_key not in node: + node[use_key] = node.use_custom_color + node[color_key] = node.color + node.use_custom_color = True + node.color = (1, 1, 1) + + else: + if color_key in node: + node.use_custom_color = node[use_key] + del node[use_key] + node.color = node[color_key] + del node[color_key] + + for n in self._tree.nodes: + _set_color(n, False) + + for node, *args in walker: + _set_color(node, use_color) + yield node, *args + class AddStatistic: # using context manager from contextlib has big overhead From 8979f0db7b60cf4f01ec2dfec40668ab1eea404a Mon Sep 17 00:00:00 2001 From: Durman Date: Mon, 23 May 2022 09:37:11 +0400 Subject: [PATCH 10/25] adopt range mode of loop node to new update system fix updating socket ids of copied nodes fix removing socket data from data cache add class for data cache debugging --- core/simple_update_system.py | 171 ++++++++++++++++++++++++----------- core/socket_data.py | 142 ++++++++++++++++++++++++++--- core/sockets.py | 5 +- node_tree.py | 4 +- nodes/logic/loop_out.py | 42 ++++++--- 5 files changed, 283 insertions(+), 81 deletions(-) diff --git a/core/simple_update_system.py b/core/simple_update_system.py index a66740dd99..d554b08164 100644 --- a/core/simple_update_system.py +++ b/core/simple_update_system.py @@ -2,7 +2,7 @@ from functools import lru_cache from graphlib import TopologicalSorter from time import perf_counter -from typing import TYPE_CHECKING, Optional, Generator, Callable +from typing import TYPE_CHECKING, Optional, Generator, Callable, Iterable from bpy.types import Node, NodeSocket, NodeTree, NodeLink from sverchok.core.events import TreeEvent @@ -58,7 +58,89 @@ def control_center(event: TreeEvent) -> Optional[Callable[[TreeEvent], Generator return Tree.update -class Tree: +class SearchTree: + def __init__(self, tree: NodeTree): + self._tree = tree + self._from_nodes: dict[SvNode, set[SvNode]] = {n: set() for n in tree.nodes} + self._to_nodes: dict[SvNode, set[SvNode]] = {n: set() for n in tree.nodes} + self._from_sock: dict[NodeSocket, NodeSocket] = dict() # only connected + self._sock_node: dict[NodeSocket, Node] = dict() # only connected sockets + self._links: set[tuple[NodeSocket, NodeSocket]] = set() # from to socket + + for link in (li for li in tree.links if not li.is_muted): + self._from_nodes[link.to_node].add(link.from_node) + self._to_nodes[link.from_node].add(link.to_node) + self._from_sock[link.to_socket] = link.from_socket + self._sock_node[link.from_socket] = link.from_node + self._sock_node[link.to_socket] = link.to_node + self._links.add((link.from_socket, link.to_socket)) + + self._remove_reroutes() + + def nodes_from(self, from_nodes: Iterable['SvNode']) -> set['SvNode']: + def node_walker_to(node_: 'SvNode'): + for nn in self._to_nodes.get(node_, []): + yield nn + + return set(bfs_walk(from_nodes, node_walker_to)) + + def nodes_to(self, to_nodes: Iterable['SvNode']) -> set['SvNode']: + def node_walker_from(node_: 'SvNode'): + for nn in self._from_nodes.get(node_, []): + yield nn + + return set(bfs_walk(to_nodes, node_walker_from)) + + def sort_nodes(self, nodes: Iterable['SvNode']) -> list['SvNode']: + walk_structure: dict[SvNode, set[SvNode]] = defaultdict(set) + for n in nodes: + if n in self._from_nodes: + walk_structure[n] = {_n for _n in self._from_nodes[n] + if _n in nodes} + nodes = [] + for node in TopologicalSorter(walk_structure).static_order(): + nodes.append(node) + return nodes + + def previous_sockets(self, node: 'SvNode') -> list[NodeSocket]: + return [self._from_sock.get(s) for s in node.inputs] + + def update_node(self, node: 'SvNode', supress=True): + with AddStatistic(node, supress): + prepare_input_data(self.previous_sockets(node), node.inputs) + node.process() + + def _remove_reroutes(self): + for _node in self._from_nodes: + if _node.bl_idname == "NodeReroute": + + # relink nodes + from_n = self._from_nodes[_node].pop() + self._to_nodes[from_n].remove(_node) # remove from + to_ns = self._to_nodes[_node] + for _next in to_ns: + self._from_nodes[_next].remove(_node) # remove to + self._from_nodes[_next].add(from_n) # add link from + self._to_nodes[from_n].add(_next) # add link to + + # relink sockets + for sock in _next.inputs: + from_s = self._from_sock.get(sock) + if from_s is None: + continue + from_s_node = self._sock_node[from_s] + if from_s_node == _node: + from_from_s = self._from_sock.get(from_s_node.inputs[0]) + if from_from_s is not None: + self._from_sock[sock] = from_from_s + else: + del self._from_sock[sock] + + self._from_nodes = {n: k for n, k in self._from_nodes.items() if n.bl_idname != 'NodeReroute'} + self._to_nodes = {n: k for n, k in self._to_nodes.items() if n.bl_idname != 'NodeReroute'} + + +class Tree(SearchTree): """It catches some data for more efficient searches compare to Blender tree data structure""" _tree_catch: dict[str, 'Tree'] = dict() # the module should be auto-reloaded to prevent crashes @@ -116,27 +198,11 @@ def mark_outdated(cls, tree: NodeTree): _tree._is_updated = False def __init__(self, tree: NodeTree): + super().__init__(tree) self._tree_catch[tree.tree_id] = self - self._tree = tree - self._from_nodes: dict[SvNode, set[SvNode]] = {n: set() for n in tree.nodes} - self._to_nodes: dict[SvNode, set[SvNode]] = {n: set() for n in tree.nodes} - self._from_sock: dict[NodeSocket, NodeSocket] = dict() # only connected - self._sock_node: dict[NodeSocket, Node] = dict() # only connected sockets - self._links: set[tuple[NodeSocket, NodeSocket]] = set() # from to socket - self._is_updated = True # False if topology was changed self._outdated_nodes: Optional[list[SvNode]] = None # None means outdated all - for link in (li for li in tree.links if not li.is_muted): - self._from_nodes[link.to_node].add(link.from_node) - self._to_nodes[link.from_node].add(link.to_node) - self._from_sock[link.to_socket] = link.from_socket - self._sock_node[link.from_socket] = link.from_node - self._sock_node[link.to_socket] = link.to_node - self._links.add((link.from_socket, link.to_socket)) - - self._remove_reroutes() - def _walk(self, outdated: list['SvNode'] = None) -> tuple[Node, list[NodeSocket]]: # walk all nodes in the tree if self._outdated_nodes is None: @@ -178,6 +244,36 @@ def node_walker(node_: 'SvNode'): nodes.append((node, [self._from_sock.get(s) for s in node.inputs])) return nodes + def __sort_nodes(self, + from_nodes: frozenset['SvNode'] = None, + to_nodes: frozenset['SvNode'] = None)\ + -> list[tuple['SvNode', list[NodeSocket]]]: + nodes_to_walk = set() + walk_structure = None + if not from_nodes and not to_nodes: + walk_structure = self._from_nodes + elif from_nodes and to_nodes: + from_ = self.nodes_from(from_nodes) + to_ = self.nodes_to(to_nodes) + nodes_to_walk = from_.intersection(to_) + elif from_nodes: + nodes_to_walk = self.nodes_from(from_nodes) + else: + nodes_to_walk = self.nodes_to(from_nodes) + + if nodes_to_walk: + walk_structure: dict[SvNode, set[SvNode]] = defaultdict(set) + for n in nodes_to_walk: + if n in self._from_nodes: + walk_structure[n] = {_n for _n in self._from_nodes[n] + if _n in nodes_to_walk} + + nodes = [] + if walk_structure: + for node in TopologicalSorter(walk_structure).static_order(): + nodes.append((node, [self._from_sock.get(s) for s in node.inputs])) + return nodes + def _update_difference(self, old: 'Tree') -> set['SvNode']: nodes_to_update = self._from_nodes.keys() - old._from_nodes.keys() new_links = self._links - old._links @@ -192,35 +288,6 @@ def _update_difference(self, old: 'Tree') -> set['SvNode']: nodes_to_update.add(old._sock_node[to_sock]) return nodes_to_update - def _remove_reroutes(self): - for _node in self._from_nodes: - if _node.bl_idname == "NodeReroute": - - # relink nodes - from_n = self._from_nodes[_node].pop() - self._to_nodes[from_n].remove(_node) # remove from - to_ns = self._to_nodes[_node] - for _next in to_ns: - self._from_nodes[_next].remove(_node) # remove to - self._from_nodes[_next].add(from_n) # add link from - self._to_nodes[from_n].add(_next) # add link to - - # relink sockets - for sock in _next.inputs: - from_s = self._from_sock.get(sock) - if from_s is None: - continue - from_s_node = self._sock_node[from_s] - if from_s_node == _node: - from_from_s = self._from_sock.get(from_s_node.inputs[0]) - if from_from_s is not None: - self._from_sock[sock] = from_from_s - else: - del self._from_sock[sock] - - self._from_nodes = {n: k for n, k in self._from_nodes.items() if n.bl_idname != 'NodeReroute'} - self._to_nodes = {n: k for n, k in self._to_nodes.items() if n.bl_idname != 'NodeReroute'} - def _debug_color(self, walker: Generator, use_color: bool = True): def _set_color(node: 'SvNode', _use_color: bool): use_key = "DEBUG_use_user_color" @@ -253,9 +320,10 @@ def _set_color(node: 'SvNode', _use_color: bool): class AddStatistic: # using context manager from contextlib has big overhead # https://stackoverflow.com/questions/26152934/why-the-staggering-overhead-50x-of-contextlib-and-the-with-statement-in-python - def __init__(self, node: 'SvNode'): + def __init__(self, node: 'SvNode', supress=True): self._node = node self._start = perf_counter() + self._supress = supress def __enter__(self): return None @@ -268,9 +336,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): else: log_error(exc_type) self._node[UPDATE_KEY] = False - self._node[ERROR_KEY] = repr(exc_type) + self._node[ERROR_KEY] = repr(exc_val) - return isinstance(exc_type, Exception) + if self._supress and exc_type is not None: + return issubclass(exc_type, Exception) def prepare_input_data(prev_socks, input_socks): diff --git a/core/socket_data.py b/core/socket_data.py index 3d283f868d..9809ec85a5 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -18,15 +18,133 @@ """For internal usage of the sockets module""" -from collections import defaultdict -from typing import Dict, NewType, Optional +from collections import UserDict +from itertools import chain +from typing import NewType, Optional, Literal +from bpy.types import NodeSocket from sverchok.core.sv_custom_exceptions import SvNoDataError +from sverchok.utils.logging import debug +from sverchok.utils.handle_blender_data import BlTrees + SockId = NewType('SockId', str) -SockContext = NewType('SockContext', str) # socket can have multiple values in case it used inside node group -DataAddress = Dict[SockId, Dict[SockContext, Optional[list]]] -socket_data_cache: DataAddress = defaultdict(lambda: defaultdict(lambda: None)) + + +class DebugMemory(UserDict): + _last_printed = dict() + + def __init__(self, data, print_all=True): + self.data = data + self._print_all = print_all + + self._id_sock: dict[SockId, NodeSocket] = dict() + + self._tree_len = 0 + self._node_len = 0 + self._sock_len = 0 + + self._data_len = 100 + + def __setitem__(self, key, value): + if key not in self.data: + self.data[key] = value + (self._pprint if self._print_all else self._pprint_id)(key, 'NEW') + else: + self.data[key] = value + (self._pprint if self._print_all else self._pprint_id)(key, 'VALUE') + + def __delitem__(self, key): + (self._pprint if self._print_all else self._pprint_id)(key, 'DELETE') + del self.data[key] + + def _pprint(self, changed_id, type_: Literal['NEW', 'DELETE', 'VALUE']): + self._update_sockets() + self._update_limits() + + print("SOCKETS DATA CACHE:") + for id_, data in self.data.items(): + data = self._cut_text(str(data), self._data_len) + if id_ == changed_id: + if type_ == 'VALUE': + data = self._colorize(str(data), "GREEN") + text = f"\t{self._to_address(id_, type_ != 'DELETE')}: {data}," + if type_ == 'NEW': + print(self._colorize(text, "GREEN")) + elif type == 'DELETE': + print(self._colorize(text, "RED")) + else: + print(text) + else: + text = f"\t{self._to_address(id_)}: {data}," + print(text) + + def _pprint_id(self, id_, type_: Literal['NEW', 'DELETE', 'VALUE']): + self._update_sockets() + self._update_limits() + + data = self.data[id_] + data = self._cut_text(str(data), self._data_len) + if type_ == 'VALUE': + data = self._colorize(str(data), "GREEN") + text = f"\t{self._to_address(id_, type_ != 'DELETE')}: {data}," + if type_ == 'NEW': + print(self._colorize(text, 'GREEN')) + elif type_ == 'DELETE': + print(self._colorize(text, 'RED')) + else: + print(text) + + def _update_sockets(self): + self._id_sock.clear() + for tree in BlTrees().sv_trees: + for node in tree.nodes: + for sock in chain(node.inputs, node.outputs): + if sock.socket_id in self._id_sock: + ds = self._id_sock[sock.socket_id] + debug(f"SOCKET ID DUPLICATION: " + f"1 - {ds.id_data.name} {ds.node.name=} {ds.name=}" + f"2 - {sock.id_data.name} {node.name=} {sock.name=}") + self._id_sock[sock.socket_id] = sock + + def _to_address(self, id_: SockId, colorize=True) -> str: + if sock := self._id_sock.get(id_): + return f"{sock.id_data.name:<{self._tree_len}}" \ + f"|{sock.node.name:<{self._node_len}}" \ + f"|{'out' if sock.is_output else 'in':<3}" \ + f"|{sock.name:<{self._sock_len}}" + else: + return self._colorize(f"NOT FOUND ID({id_})", "YELLOW" if colorize else None) + + def _update_limits(self): + for sock in self._id_sock.values(): + self._tree_len = max(self._tree_len, len(sock.id_data.name)) + self._node_len = max(self._node_len, len(sock.node.name)) + self._sock_len = max(self._sock_len, len(sock.name)) + + @staticmethod + def _colorize(text, color: Optional[Literal['GREEN', 'RED', 'YELLOW']] = None): + if not color: + return text + elif color == 'GREEN': + return f"\033[32m{text}\033[0m" + elif color == 'RED': + return f"\033[31m{text}\033[0m" + elif color == 'YELLOW': + return f"\033[33m{text}\033[0m" + + @staticmethod + def _cut_text(text, max_size): + if len(text) < max_size: + return text + else: + start = text[:max_size//2-2] + end = text[len(text) - (max_size//2-1):] + return f"{start}...{end}" + + +socket_data_cache: dict[SockId, list] = dict() +# socket_data_cache = DebugMemory(socket_data_cache, False) def sv_deep_copy(lst): @@ -50,25 +168,25 @@ def sv_forget_socket(socket): pass -def sv_set_socket(socket, data, context: SockContext = ''): +def sv_set_socket(socket, data): """sets socket data for socket""" - socket_data_cache[socket.socket_id][context] = data + socket_data_cache[socket.socket_id] = data -def sv_get_socket(socket, deepcopy=True, context: SockContext = ''): +def sv_get_socket(socket, deepcopy=True): """gets socket data from socket, if deep copy is True a deep copy is make_dep_dict, to increase performance if the node doesn't mutate input set to False and increase performance substanstilly """ - data = socket_data_cache[socket.socket_id][context] + data = socket_data_cache.get(socket.socket_id) if data is not None: return sv_deep_copy(data) if deepcopy else data else: raise SvNoDataError(socket) -def get_output_socket_data(node, output_socket_name, context: SockContext = ''): +def get_output_socket_data(node, output_socket_name): """ This method is intended to usage in internal tests mainly. Get data that the node has written to the output socket. @@ -76,8 +194,8 @@ def get_output_socket_data(node, output_socket_name, context: SockContext = ''): """ socket = node.inputs[output_socket_name] # todo why output? sock_address = socket.socket_id - if sock_address in socket_data_cache and context in socket_data_cache[sock_address]: - return socket_data_cache[sock_address][context] + if sock_address in socket_data_cache: + return socket_data_cache[sock_address] else: raise SvNoDataError(socket) diff --git a/core/sockets.py b/core/sockets.py index fc65224733..57d0edfaca 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -399,7 +399,7 @@ def hide_safe(self, value): self.hide = value - def sv_get(self, default=..., deepcopy=True): # todo should be removed, data should path directly to process method + def sv_get(self, default=..., deepcopy=True): """ The method is used for getting socket data In most cases the method should not be overridden @@ -434,7 +434,7 @@ def sv_get(self, default=..., deepcopy=True): # todo should be removed, data sh raise SvNoDataError(self) - def sv_set(self, data): # todo should be removed + def sv_set(self, data): """Set data, provide context in case the node can be evaluated several times in different context""" if self.is_output: data = self.postprocess_output(data) @@ -447,6 +447,7 @@ def sv_forget(self): def replace_socket(self, new_type, new_name=None): """Replace a socket with a socket of new_type and keep links, return the new socket, the old reference might be invalid""" + self.sv_forget() return replace_socket(self, new_type, new_name) def draw_property(self, layout, prop_origin=None, prop_name='default_property'): diff --git a/node_tree.py b/node_tree.py index e6cdcb4d3c..9984ac7d5a 100644 --- a/node_tree.py +++ b/node_tree.py @@ -285,7 +285,7 @@ def free(self): self.sv_free() # free sockets memory - for s in self.outputs: + for s in chain(self.inputs, self.outputs): s.sv_forget() # remove tree space drawings @@ -294,6 +294,8 @@ def free(self): def copy(self, original): """Called upon the node being copied""" self.n_id = "" + for sock in chain(self.inputs, self.outputs): + sock.s_id = '' self.sv_copy(original) def update(self): diff --git a/nodes/logic/loop_out.py b/nodes/logic/loop_out.py index 3efa8bd71d..c273995cd4 100644 --- a/nodes/logic/loop_out.py +++ b/nodes/logic/loop_out.py @@ -17,13 +17,15 @@ # ##### END GPL LICENSE BLOCK ##### import bpy -from bpy.props import EnumProperty, BoolProperty +from bpy.props import EnumProperty +from sverchok.core.simple_update_system import Tree from sverchok.node_tree import SverchCustomTreeNode from sverchok.core.update_system import make_tree_from_nodes, do_update -from sverchok.data_structure import list_match_func, enum_item_4, updateNode +from sverchok.data_structure import list_match_func, enum_item_4 + def process_looped_nodes(node_list, tree_nodes, process_name, iteration): for node_name in node_list: @@ -283,34 +285,44 @@ def range_mode(self, loop_in_node): for inp, outp in zip(self.inputs[2:], self.outputs): outp.sv_set(inp.sv_get(deepcopy=False, default=[])) else: - - intersection, related_nodes = self.get_affected_nodes(loop_in_node) - if self.bad_inner_loops(intersection): + tree = Tree.get(self.id_data) + from_nodes = tree.nodes_from([loop_in_node]) + to_nodes = tree.nodes_to([self]) + loop_nodes = from_nodes.intersection(to_nodes) + sort_loop_nodes = tree.sort_nodes(loop_nodes) + break_socket = tree.previous_sockets(self)[1] + + if self.bad_inner_loops((n.name for n in loop_nodes)): # todo pass real nodes raise Exception("Loops inside not well connected") do_print = loop_in_node.print_to_console - tree_nodes = self.id_data.nodes - do_update(intersection[:-1], tree_nodes) + # the nodes should be cleared out from last loop data + for node in sort_loop_nodes[:-1]: + tree.update_node(node) for i in range(iterations-1): - if self.break_loop(): + if break_socket and break_socket.sv_get(default=[[False]])[0][0]: break - for j, socket in enumerate(self.inputs[2:]): + for j, socket in enumerate(tree.previous_sockets(self)[2:]): data = socket.sv_get(deepcopy=False, default=[]) loop_in_node.outputs[j+3].sv_set(data) loop_in_node.outputs['Loop Number'].sv_set([[i+1]]) if do_print: print(f"Looping iteration Number {i+1}") - process_looped_nodes(intersection[1:-1], tree_nodes, 'Iteration', i+1) + for node in sort_loop_nodes[1:-1]: + try: + tree.update_node(node, supress=False) + except Exception: + raise Exception(f"Iteration number: {i+1}") - - for inp, outp in zip(self.inputs[2:], self.outputs): + for inp, outp in zip(tree.previous_sockets(self)[2:], self.outputs): outp.sv_set(inp.sv_get(deepcopy=False, default=[])) - do_update(related_nodes, self.id_data.nodes) - - + from_out_nodes = tree.nodes_from([self]) + side_loop_nodes = from_nodes - from_out_nodes - loop_nodes + for node in tree.sort_nodes(side_loop_nodes): + tree.update_node(node) def register(): From 2e6ffaa6578028b2ed06117dd985c427f49fad3b Mon Sep 17 00:00:00 2001 From: Durman Date: Mon, 23 May 2022 14:59:49 +0400 Subject: [PATCH 11/25] update for each mode to work with new update system --- nodes/logic/loop_out.py | 78 +++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/nodes/logic/loop_out.py b/nodes/logic/loop_out.py index c273995cd4..b094ce1f7b 100644 --- a/nodes/logic/loop_out.py +++ b/nodes/logic/loop_out.py @@ -23,17 +23,9 @@ from sverchok.node_tree import SverchCustomTreeNode -from sverchok.core.update_system import make_tree_from_nodes, do_update from sverchok.data_structure import list_match_func, enum_item_4 -def process_looped_nodes(node_list, tree_nodes, process_name, iteration): - for node_name in node_list: - try: - tree_nodes[node_name].process() - except Exception as e: - raise type(e)(str(e) + f' @ {node_name} node. {process_name} number: {iteration}') - socket_labels = {'Range': 'Break', 'For_Each': 'Skip'} class SvUpdateLoopOutSocketLabels(bpy.types.Operator): @@ -186,20 +178,6 @@ def bad_inner_loops(self, intersection): return False - def get_affected_nodes(self, loop_in_node): - tree = self.id_data - nodes_to_loop_out = make_tree_from_nodes([self.name], tree, down=False) - nodes_from_loop_in = make_tree_from_nodes([loop_in_node.name], tree, down=True) - nodes_from_loop_out = make_tree_from_nodes([self.name], tree, down=True) - - set_nodes_from_loop_in = frozenset(nodes_from_loop_in) - set_nodes_from_loop_out = frozenset(nodes_from_loop_out) - - intersection = [x for x in nodes_to_loop_out if x in set_nodes_from_loop_in and tree.nodes[x].bl_idname != 'NodeReroute'] - related_nodes = [x for x in nodes_from_loop_in if x not in set_nodes_from_loop_out and x not in intersection] - - return intersection, related_nodes - def ready(self): if not self.inputs[0].is_linked: print("Inner Loop not connected") @@ -224,38 +202,43 @@ def process(self): else: self.for_each_mode(loop_in_node) - def break_loop(self): - stop_ = self.inputs['Break'].sv_get(deepcopy=False, default=[[False]]) - return stop_[0][0] - - def append_data(self, out_data): - if not self.break_loop(): - for inp, out in zip(self.inputs[2:len(self.outputs)+2], out_data): - out.append(inp.sv_get(deepcopy=False, default=[[]])[0]) - def for_each_mode(self, loop_in_node): - list_match = list_match_func[loop_in_node.list_match] params = list_match([inp.sv_get(deepcopy=False, default=[]) for inp in loop_in_node.inputs[1:-1]]) if len(params[0]) == 1: - if not self.break_loop(): + if not self.inputs['Break'].sv_get(deepcopy=False, default=[[False]])[0][0]: for inp, outp in zip(self.inputs[2:], self.outputs): outp.sv_set(inp.sv_get(deepcopy=False, default=[])) else: for outp in self.outputs: outp.sv_set([]) else: - intersection, related_nodes = self.get_affected_nodes(loop_in_node) - if self.bad_inner_loops(intersection): + tree = Tree.get(self.id_data) + from_nodes = tree.nodes_from([loop_in_node]) + to_nodes = tree.nodes_to([self]) + loop_nodes = from_nodes.intersection(to_nodes) + sort_loop_nodes = tree.sort_nodes(loop_nodes) + break_socket = tree.previous_sockets(self)[1] + + if self.bad_inner_loops((n.name for n in loop_nodes)): raise Exception("Loops inside not well connected") - tree_nodes = self.id_data.nodes do_print = loop_in_node.print_to_console idx = 0 out_data = [[] for inp in self.inputs[2:]] - do_update(intersection[:-1], tree_nodes) - self.append_data(out_data) + + # the nodes should be cleared out from last loop data + for node in sort_loop_nodes[:-1]: + tree.update_node(node) + + if not break_socket or not break_socket.sv_get(default=[[False]])[0][0]: + for inp, out in zip(tree.previous_sockets(self)[2:len(self.outputs) + 2], out_data): + if inp is not None: + out.append(inp.sv_get()[0]) + else: + out.append([]) + for item_params in zip(*params): if idx == 0: idx += 1 @@ -266,13 +249,26 @@ def for_each_mode(self, loop_in_node): idx += 1 if do_print: print(f"Looping Object Number {idx}") - process_looped_nodes(intersection[1:-1], tree_nodes, 'Element', idx) - self.append_data(out_data) + for node in sort_loop_nodes[1:-1]: + try: + tree.update_node(node, supress=False) + except Exception: + raise Exception(f"Element: {idx}") + + if not break_socket or not break_socket.sv_get(default=[[False]])[0][0]: + for inp, out in zip(tree.previous_sockets(self)[2:len(self.outputs) + 2], out_data): + if inp is not None: + out.append(inp.sv_get()[0]) + else: + out.append([]) for inp, outp in zip(out_data, self.outputs): outp.sv_set(inp) - do_update(related_nodes, self.id_data.nodes) + from_out_nodes = tree.nodes_from([self]) + side_loop_nodes = from_nodes - from_out_nodes - loop_nodes + for node in tree.sort_nodes(side_loop_nodes): + tree.update_node(node) def range_mode(self, loop_in_node): iterations = min(int(loop_in_node.inputs['Iterations'].sv_get()[0][0]), loop_in_node.max_iterations) From bcc89ba04575d7939a6dcd365557a8c91de42ccf Mon Sep 17 00:00:00 2001 From: Durman Date: Mon, 23 May 2022 16:32:04 +0400 Subject: [PATCH 12/25] get into account Live update property of trees --- core/simple_update_system.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/simple_update_system.py b/core/simple_update_system.py index d554b08164..2459537616 100644 --- a/core/simple_update_system.py +++ b/core/simple_update_system.py @@ -156,7 +156,7 @@ def get(cls, tree: NodeTree) -> 'Tree': _tree = cls(tree) if old._outdated_nodes is not None: _tree._outdated_nodes = old._outdated_nodes.copy() - _tree._outdated_nodes.extend(_tree._update_difference(old)) + _tree._outdated_nodes.update(_tree._update_difference(old)) return _tree @classmethod @@ -173,8 +173,13 @@ def update_animation(cls, event: TreeEvent): def update(cls, event: TreeEvent, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: if update_nodes: tree = cls.get(event.tree) + + if not event.tree.sv_process and event.type in {event.TREE_UPDATE, event.NODES_UPDATE, event.SCENE_UPDATE}: + tree._outdated_nodes.update(event.updated_nodes) + return + walker = tree._walk(list(event.updated_nodes or [])) - # walker = tree._debug_color(walker) + walker = tree._debug_color(walker) for node, prev_socks in walker: with AddStatistic(node): yield node @@ -201,13 +206,13 @@ def __init__(self, tree: NodeTree): super().__init__(tree) self._tree_catch[tree.tree_id] = self self._is_updated = True # False if topology was changed - self._outdated_nodes: Optional[list[SvNode]] = None # None means outdated all + self._outdated_nodes: Optional[set[SvNode]] = None # None means outdated all def _walk(self, outdated: list['SvNode'] = None) -> tuple[Node, list[NodeSocket]]: # walk all nodes in the tree if self._outdated_nodes is None: outdated = None - self._outdated_nodes = [] + self._outdated_nodes = set() # walk triggered nodes and error nodes from previous updates else: outdated.extend(self._outdated_nodes) @@ -219,7 +224,7 @@ def _walk(self, outdated: list['SvNode'] = None) -> tuple[Node, list[NodeSocket] if all(n.get(UPDATE_KEY, True) for sock in other_socks if (n := self._sock_node.get(sock))): yield node, other_socks if node.get(ERROR_KEY, False): - self._outdated_nodes.append(node) + self._outdated_nodes.add(node) else: node[UPDATE_KEY] = False From a89e4cc9575756c1b0e62834e4e50b7a0b286dd6 Mon Sep 17 00:00:00 2001 From: Durman Date: Tue, 24 May 2022 09:03:55 +0400 Subject: [PATCH 13/25] adopt evolver node to new update system remove module with old update system --- core/__init__.py | 2 +- core/simple_update_system.py | 2 +- core/update_system.py | 399 --------------------------------- nodes/logic/evolver.py | 49 ++-- tests/intersect_edges_tests.py | 1 - tests/update_system_tests.py | 37 --- 6 files changed, 23 insertions(+), 467 deletions(-) delete mode 100644 core/update_system.py delete mode 100644 tests/update_system_tests.py diff --git a/core/__init__.py b/core/__init__.py index a954cb7d2b..d870013092 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -12,7 +12,7 @@ core_modules = [ "sv_custom_exceptions", "simple_update_system", "sockets", "socket_data", - "handlers", "update_system", "main_tree_handler", + "handlers", "main_tree_handler", "events", "node_group", "group_handlers" ] diff --git a/core/simple_update_system.py b/core/simple_update_system.py index 2459537616..e886b711a9 100644 --- a/core/simple_update_system.py +++ b/core/simple_update_system.py @@ -179,7 +179,7 @@ def update(cls, event: TreeEvent, update_nodes=True, update_interface=True) -> G return walker = tree._walk(list(event.updated_nodes or [])) - walker = tree._debug_color(walker) + # walker = tree._debug_color(walker) for node, prev_socks in walker: with AddStatistic(node): yield node diff --git a/core/update_system.py b/core/update_system.py deleted file mode 100644 index e09e758515..0000000000 --- a/core/update_system.py +++ /dev/null @@ -1,399 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -import collections -import time -from itertools import chain - -import bpy - -from sverchok import data_structure -from sverchok.core.sv_custom_exceptions import SvNoDataError -from sverchok.utils.logging import warning, error, exception -from sverchok.utils.profile import profile -import sverchok - -import ast - -graphs = [] -# graph_dicts = {} - -no_data_color = (1, 0.3, 0) -exception_color = (0.8, 0.0, 0) - - -def update_error_colors(self, context): - global no_data_color - global exception_color - no_data_color = self.no_data_color[:] - exception_color = self.exception_color[:] - -def reset_timing_graphs(): - global graphs - graphs = [] - # graph_dicts = {} - - -# cache node group update trees -update_cache = {} -# cache for partial update lists -partial_update_cache = {} - - -def make_dep_dict(node_tree, down=False): - """ - Create a dependency dictionary for node group. - """ - ng = node_tree - - deps = collections.defaultdict(set) - - # create wifi out dependencies, process if needed - - wifi_out_nodes = [(name, node.var_name) - for name, node in ng.nodes.items() - if node.bl_idname == 'WifiOutNode' and node.outputs] - if wifi_out_nodes: - wifi_dict = {node.var_name: name - for name, node in ng.nodes.items() - if node.bl_idname == 'WifiInNode'} - - for i,link in enumerate(list(ng.links)): - # this protects against a rare occurrence where - # a link is considered valid without a to_socket - # or a from_socket. protects against a blender crash - # see https://github.com/nortikin/sverchok/issues/493 - - if not (link.to_socket and link.from_socket): - ng.links.remove(link) - raise ValueError("Invalid link found!, please report this file") - # it seems to work even with invalid links, maybe because sverchok update is independent from blender update - # if not link.is_valid: - # return collections.defaultdict(set) # this happens more often than one might think - if link.is_hidden: - continue - key, value = (link.from_node.name, link.to_node.name) if down else (link.to_node.name, link.from_node.name) - deps[key].add(value) - - for name, var_name in wifi_out_nodes: - other = wifi_dict.get(var_name) - if not other: - warning("Unsatisifed Wifi dependency: node, %s var,%s", name, var_name) - return collections.defaultdict(set) - if down: - deps[other].add(name) - else: - deps[name].add(other) - - return deps - - -def make_update_list(node_tree, node_set=None, dependencies=None): - """ - Makes a update list from a node_group - if a node set is passed only the subtree defined by the node set is used. Otherwise - the complete node tree is used. - If dependencies are not passed they are built. - """ - - ng = node_tree - if not node_set: # if no node_set, take all - node_set = set(ng.nodes.keys()) - if len(node_set) == 1: - return list(node_set) - if node_set: # get one name - name = node_set.pop() - node_set.add(name) - else: - return [] - if not dependencies: - deps = make_dep_dict(ng) - else: - deps = dependencies - - tree_stack = collections.deque([name]) - tree_stack_append = tree_stack.append - tree_stack_pop = tree_stack.pop - out = collections.OrderedDict() - # travel in node graph create one sorted list of nodes based on dependencies - node_count = len(node_set) - while node_count > len(out): - node_dependencies = True - for dep_name in deps[name]: - if dep_name in node_set and dep_name not in out: - tree_stack_append(name) - name = dep_name - node_dependencies = False - break - if len(tree_stack) > node_count: - error("Invalid node tree!") - return [] - # if all dependencies are in out - if node_dependencies: - if name not in out: - out[name] = 1 - if tree_stack: - name = tree_stack_pop() - else: - if node_count == len(out): - break - for node_name in node_set: - if node_name not in out: - name = node_name - break - return list(out.keys()) - - -def separate_nodes(ng, links=None): - ''' - Separate a node group (layout) into unconnected parts - Arguments: Node group - Returns: A list of sets with separate node groups - ''' - nodes = set(ng.nodes.keys()) - if not nodes: - return [] - node_links = make_dep_dict(ng) - down = make_dep_dict(ng, down=True) - for name, links in down.items(): - node_links[name].update(links) - n = nodes.pop() - node_set_list = [set([n])] - node_stack = collections.deque() - - # find separate sets - node_stack_append = node_stack.append - node_stack_pop = node_stack.pop - - while nodes: - for node in node_links[n]: - if node not in node_set_list[-1]: - node_stack_append(node) - if not node_stack: # new part - n = nodes.pop() - node_set_list.append(set([n])) - else: - while n in node_set_list[-1] and node_stack: - n = node_stack_pop() - nodes.discard(n) - node_set_list[-1].add(n) - - found_node_sets = [ns for ns in node_set_list if len(ns) > 1] - - if hasattr(ng, "sv_subtree_evaluation_order"): - sorting_type = ng.sv_subtree_evaluation_order - if sorting_type in {'X', 'Y'}: - sort_index = 0 if sorting_type == "X" else 1 - find_lowest = lambda ns: min(ng.nodes[n].absolute_location[sort_index] for n in ns) - found_node_sets = sorted(found_node_sets, key=find_lowest) - - return found_node_sets - -def make_tree_from_nodes(node_names, tree, down=True): - """ - Create a partial update list from a sub-tree, node_names is a list of nodes that - drives change for the tree - """ - ng = tree - nodes = ng.nodes - if not node_names: - warning("No nodes!") - return make_update_list(ng) - - out_set = set(node_names) - - out_stack = collections.deque(node_names) - current_node = out_stack.pop() - - # build downwards links, this should be cached perhaps - node_links = make_dep_dict(ng, down) - while current_node: - for node in node_links[current_node]: - if node not in out_set: - out_set.add(node) - out_stack.append(node) - if out_stack: - current_node = out_stack.pop() - else: - current_node = '' - - if len(out_set) == 1: - return list(out_set) - else: - return make_update_list(ng, out_set) - - -# to make update tree based on node types and node names bases -# no used yet -# should add a check do find animated or driven nodes. -# needs some updates - - -def update_error_nodes(ng, name, err=Exception): - if "error nodes" in ng: - error_nodes = ast.literal_eval(ng["error nodes"]) - else: - error_nodes = {} - - node = ng.nodes.get(name) - if not node: - return - error_nodes[name] = (node.use_custom_color, node.color[:]) - ng["error nodes"] = str(error_nodes) - - if isinstance(err, SvNoDataError): - node.color = no_data_color - else: - node.color = exception_color - node.use_custom_color=True - - -def reset_error_node(ng, name): - node = ng.nodes.get(name) - if node: - if "error nodes" in ng: - error_nodes = ast.literal_eval(ng["error nodes"]) - if name in error_nodes: - node.use_custom_color, node.color = error_nodes[name] - del error_nodes[name] - ng["error nodes"] = str(error_nodes) - -def reset_error_nodes(ng): - if "error nodes" in ng: - error_nodes = ast.literal_eval(ng["error nodes"]) - for name, data in error_nodes.items(): - node = ng.nodes.get(name) - if node: - node.use_custom_color = data[0] - node.color = data[1] - del ng["error nodes"] - -def node_info(ng_name, node, start, delta): - return {"name" : node.name, "bl_idname": node.bl_idname, "start": start, "duration": delta, "tree_name": ng_name} - -@profile(section="UPDATE") -def do_update_general(node_list, nodes, procesed_nodes=set()): - """ - General update function for node set - """ - ng = nodes.id_data - - global graphs - # graph_dicts[ng.name] = [] - timings = [] - graph = [] - gather = graph.append - - total_time = 0 - done_nodes = set(procesed_nodes) - - for node_name in node_list: - if node_name in done_nodes: - continue - try: - node = nodes[node_name] - start = time.perf_counter() - if hasattr(node, "process"): - node.process() - - delta = time.perf_counter() - start - total_time += delta - - timings.append(delta) - gather(node_info(ng.name, node, start, delta)) - - # probably it's not great place for doing this, the node can be a ReRoute - [s.update_objects_number() for s in chain(node.inputs, node.outputs) if hasattr(s, 'update_objects_number')] - - except Exception as err: - update_error_nodes(ng, node_name, err) - #traceback.print_tb(err.__traceback__) - exception("Node %s had exception: %s", node_name, err) - return None - - graphs.append(graph) - - # graph_dicts[nodes.id_data.name] = graph - return timings - - -def do_update(node_list, nodes): - do_update_general(node_list, nodes) - -def build_update_list(ng=None): - """ - Makes a complete update list for the tree, - If tree is not passed, all sverchok custom tree - are processced - """ - global update_cache - global partial_update_cache - reset_timing_graphs() - - if not ng: - for ng in sverchok_trees(): - build_update_list(ng) - else: - node_sets = separate_nodes(ng) - deps = make_dep_dict(ng) - out = [make_update_list(ng, s, deps) for s in node_sets] - update_cache[ng.name] = out - partial_update_cache[ng.name] = {} - # reset_socket_cache(ng) - - -def sverchok_trees(): - for ng in bpy.data.node_groups: - if ng.bl_idname == "SverchCustomTreeType": - yield ng - -def process_tree(ng=None): - global update_cache - global partial_update_cache - reset_timing_graphs() - - if data_structure.RELOAD_EVENT: - reload_sverchok() - #return - if not ng: - for ng in sverchok_trees(): - process_tree(ng) - elif ng.bl_idname == "SverchCustomTreeType" and ng.sv_process: - update_list = update_cache.get(ng.name) - reset_error_nodes(ng) - if not update_list: - build_update_list(ng) - update_list = update_cache.get(ng.name) - for l in update_list: - do_update(l, ng.nodes) - else: - pass - - -def reload_sverchok(): - data_structure.RELOAD_EVENT = False - from sverchok.core import handlers - handlers.sv_post_load([]) - reset_timing_graphs() - - -def register(): - addon_name = sverchok.__name__ - addon = bpy.context.preferences.addons.get(addon_name) - if addon: - update_error_colors(addon.preferences, []) diff --git a/nodes/logic/evolver.py b/nodes/logic/evolver.py index 181cc0585c..217dc58448 100644 --- a/nodes/logic/evolver.py +++ b/nodes/logic/evolver.py @@ -10,7 +10,7 @@ import random import time from collections import namedtuple -from typing import NamedTuple +from typing import NamedTuple, Union import numpy as np import bpy @@ -18,9 +18,9 @@ from bpy.props import ( BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty) +from sverchok.core.simple_update_system import Tree from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode -from sverchok.core.update_system import make_tree_from_nodes, do_update from sverchok.utils.sv_operator_mixins import SvGenericNodeLocator from sverchok.utils.listutils import ( listinput_getI, @@ -292,7 +292,7 @@ def is_valid_node(node, genotype_frame): return False -def get_genes(target_tree, genotype_frame): +def get_genes(target_tree, genotype_frame) -> list[Union[NumberGene, ListInputGene, NumberMultiGene]]: genes = [] for node in target_tree.nodes: if is_valid_node(node, genotype_frame): @@ -356,16 +356,19 @@ def fill_genes(self, random_val=True): agent_gene = gene.init_val self.genes.append(agent_gene) - - - def evaluate_fitness(self, tree, update_list, node): + def evaluate_fitness(self, tree, node, s_tree: Tree, exec_order): try: tree.sv_process = False for gen_data, agent_gene in zip(self.genes_def, self.genes): gen_data.set_node_with_gene(tree, agent_gene) tree.sv_process = True - do_update(update_list, tree.nodes) + for node in exec_order: + try: + s_tree.update_node(node, supress=False) + except Exception: + raise + agent_fitness = node.inputs[0].sv_get(deepcopy=False)[0] if isinstance(agent_fitness, list): agent_fitness = agent_fitness[0] @@ -404,10 +407,12 @@ def __init__(self, genotype_frame, node, tree): self.tree = tree self.time_start = time.time() self.genes = get_genes(tree, genotype_frame) - self.update_list = make_tree_from_nodes([g.name for g in self.genes], tree) - self.population_g = [] + self.population_g: list[DNA] = [] self.init_population(node.population_n) + self._tree = Tree.get(tree) + exec_order = self._tree.nodes_from([tree.nodes[g.name] for g in self.genes]) + self.exec_order = self._tree.sort_nodes(exec_order) def init_population(self, population_n): @@ -429,12 +434,13 @@ def init_population_from_previous(self, population_n): for i in range(population_n-len(previous_population)): self.population_g.append(DNA(self.genes)) - def evaluate_fitness_g(self): + def evaluate_fitness_g(self): try: for agent in self.population_g: - agent.evaluate_fitness(self.tree, self.update_list, self.node) + agent.evaluate_fitness(self.tree, self.node, self._tree, self.exec_order) finally: self.tree.sv_process = True + def population_genes(self): return [agent.genes for agent in self.population_g] @@ -548,21 +554,8 @@ def sv_execute(self, context, node): np.random.seed(node.r_seed) population = Population(genotype_frame, node, tree) population.evolve() - update_list = make_tree_from_nodes([node.name], tree) - do_update(update_list, tree.nodes) - + node.process_node(None) -def set_fittest(tree, genes, agent, update_list): - '''sets the nodetree with the best value''' - try: - tree.sv_process = False - for gen_data, agent_gene in zip(genes, agent): - gen_data.set_node_with_gene(tree, agent_gene) - - tree.sv_process = True - do_update(update_list, tree.nodes) - finally: - tree.sv_process = True class SvEvolverSetFittest(bpy.types.Operator, SvGenericNodeLocator): @@ -571,12 +564,12 @@ class SvEvolverSetFittest(bpy.types.Operator, SvGenericNodeLocator): def sv_execute(self, context, node): tree = node.id_data - + data = evolver_mem[node.node_id] genes = data["genes"] population = data["population"] - update_list = make_tree_from_nodes([g.name for g in genes], tree) - set_fittest(tree, genes, population[0], update_list) + for gen_data, agent_gene in zip(genes, population[0]): + gen_data.set_node_with_gene(tree, agent_gene) def get_framenodes(base_node, _): diff --git a/tests/intersect_edges_tests.py b/tests/intersect_edges_tests.py index 3b55f84f1c..cc8ef57ff3 100644 --- a/tests/intersect_edges_tests.py +++ b/tests/intersect_edges_tests.py @@ -1,4 +1,3 @@ -from sverchok.core.update_system import process_tree from sverchok.utils.testing import * diff --git a/tests/update_system_tests.py b/tests/update_system_tests.py deleted file mode 100644 index f8d94b8a00..0000000000 --- a/tests/update_system_tests.py +++ /dev/null @@ -1,37 +0,0 @@ - -import collections -import unittest - -from sverchok.utils.testing import * -from sverchok.utils.logging import debug, info -from sverchok.core.update_system import make_dep_dict, make_update_list -#from sverchok.tests.mocks import * - -class UpdateSystemTests(ReferenceTreeTestCase): - - reference_file_name = "complex_1_ref.blend.gz" - - def test_make_dep_dict(self): - tree = get_node_tree() - result = make_dep_dict(tree) - # info(result) - - expected_result = {'Bevel': {'Box'}, 'Move': {'Vector in', 'Bevel.001'}, 'Viewer Draw': {'Move', 'Bevel.001'}, 'Extrude Separate Faces.002': {'Box'}, 'Bevel.001': {'Extrude Separate Faces.002'}, 'Viewer Draw.001': {'Bevel'}} - - #info("Dict: %s", result) - self.assertEqual(result, expected_result) - - def test_make_update_list(self): - tree = get_node_tree() - result = make_update_list(tree) - #info(result) - - # We can't test for exact equality of result and some expected_result, - # because exact order depends on order of items in defaultdict(), - # which may change from one run to another. - # So we just test that each node is updated after all its dependencies. - for node, deps in make_dep_dict(tree).items(): - node_idx = result.index(node) - for dep in deps: - dep_idx = result.index(dep) - self.assertTrue(dep_idx < node_idx) From f268d4f62cdc87cb738cab53e3e4b6e49e73c185 Mon Sep 17 00:00:00 2001 From: Durman Date: Wed, 25 May 2022 11:46:44 +0400 Subject: [PATCH 14/25] Adopt node groups to new update system to call from main trees --- core/__init__.py | 2 +- core/main_tree_handler.py | 9 ++- core/node_group.py | 75 ++++++++++--------- core/socket_data.py | 4 +- ...mple_update_system.py => update_system.py} | 43 ++++++++--- nodes/list_struct/slice.py | 8 +- nodes/logic/evolver.py | 2 +- nodes/logic/loop_out.py | 2 +- 8 files changed, 87 insertions(+), 58 deletions(-) rename core/{simple_update_system.py => update_system.py} (90%) diff --git a/core/__init__.py b/core/__init__.py index d870013092..e7994faf8d 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -10,7 +10,7 @@ ] core_modules = [ - "sv_custom_exceptions", "simple_update_system", + "sv_custom_exceptions", "update_system", "sockets", "socket_data", "handlers", "main_tree_handler", "events", "node_group", "group_handlers" diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index 4f4eb1553b..f8b3ac6e80 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -22,7 +22,7 @@ from sverchok.utils.tree_structure import Tree, Node from sverchok.utils.handle_blender_data import BlTrees, BlNode from sverchok.utils.profile import profile -import sverchok.core.simple_update_system as sus +import sverchok.core.update_system as sus if TYPE_CHECKING: from sverchok.core.node_group import SvGroupTreeNode as SvNode @@ -36,7 +36,7 @@ class TreeHandler: @staticmethod def send(event: TreeEvent): """Control center""" - # debug(event.type) + # print(f"{event.type=}, {event.tree=}") current_task = Task.get() # this should be first other wise other instructions can spoil the node statistic to redraw @@ -226,8 +226,11 @@ def finish_task(self): status = self._event.tree.nodes[-1].use_custom_color self._event.tree.nodes[-1].use_custom_color = not status self._event.tree.nodes[-1].use_custom_color = status + # this indicates that process of the tree is finished and next scene event can be skipped - self._event.tree['SKIP_UPDATE'] = True + # the scene trigger will try to update all trees, so they all should be marked + for t in BlTrees().sv_main_trees: + t['SKIP_UPDATE'] = True gc.enable() debug(f'Global update - {int((time() - self._start_time) * 1000)}ms') diff --git a/core/node_group.py b/core/node_group.py index c8a6cfe00f..540e2d6c3e 100644 --- a/core/node_group.py +++ b/core/node_group.py @@ -20,10 +20,10 @@ from sverchok.core.group_handlers import MainHandler from sverchok.core.sockets import socket_type_names from sverchok.core.events import GroupEvent +from sverchok.core.update_system import ERROR_KEY, Tree as UpdateTree from sverchok.utils.tree_structure import Tree, Node from sverchok.utils.sv_node_utils import recursive_framed_location_finder from sverchok.utils.handle_blender_data import BlNode, BlTrees -from sverchok.utils.logging import catch_log_error from sverchok.node_tree import UpdateNodes, SvNodeTreeCommon, SverchCustomTreeNode @@ -34,6 +34,7 @@ class SvGroupTree(SvNodeTreeCommon, bpy.types.NodeTree): bl_label = 'Group tree' handler = MainHandler + sv_process = True # for consistency with main tree # should be updated by "Go to edit group tree" operator group_node_name: bpy.props.StringProperty(options={'SKIP_SAVE'}) @@ -372,34 +373,44 @@ def draw_buttons(self, context, layout): else: row_search.operator('node.add_group_tree', text='New', icon='ADD') - def process(self): # todo to remove + def process(self): """ This method is going to be called only by update system of main tree Calling this method means that input group node should fetch data from group node - (should be updated for current context) """ # it's better process the node even if it is switched off in case when tree is just opened - # todo it requires socket API changes first should_update_output_data = False - # if self.outputs: - # try: - # self.outputs[0].sv_get(deepcopy=False) - # except LookupError: - # should_update_output_data = True + if self.outputs: + try: + self.outputs[0].sv_get(deepcopy=False) + except LookupError: + should_update_output_data = True if not self.node_tree or (not self.is_active and not should_update_output_data): return self.node_tree: SvGroupTree input_node = self.active_input() - updater = self.node_tree.handler.update(GroupEvent(GroupEvent.GROUP_NODE_UPDATE, - group_nodes_path=[self], - updated_nodes=[input_node] if input_node else [])) - list(updater) - errors = self.node_tree.handler.get_error_nodes([self]) - for error in errors: - if error: - raise error + output_node = self.active_output() + if not input_node or not output_node: + return + + for in_s, out_s in zip(self.inputs, input_node.outputs): + if out_s.identifier == '__extend__': # virtual socket + break + out_s.sv_set(in_s.sv_get(deepcopy=False)) + + tree = UpdateTree.get(self.node_tree) + tree.update([input_node]) + + for node in self.node_tree.nodes: + if err := node.get(ERROR_KEY): + raise Exception(err) + else: + for in_s, out_s in zip(output_node.inputs, self.outputs): + if in_s.identifier == '__extend__': # virtual socket + break + out_s.sv_set(in_s.sv_get(deepcopy=False)) def updater(self, group_nodes_path: Optional[List['SvGroupTreeNode']] = None, is_input_changed: bool = True, @@ -433,6 +444,11 @@ def active_input(self) -> Optional[bpy.types.Node]: if node.bl_idname == 'NodeGroupInput': return node + def active_output(self) -> Optional[bpy.types.Node]: + for node in reversed(self.node_tree.nodes): + if node.bl_idname == 'NodeGroupOutput': + return node + def update(self): if 'init_tree' in self.id_data: # tree is building by a script - let it do this return @@ -907,29 +923,16 @@ def invoke(self, context, event): AddNodeOutputInput, AddGroupTreeFromSelected, SearchGroupTree, UngroupGroupTree] -class BaseInOutNodes: - - # update system should handle it so node could know context of its evaluation - call_path: bpy.props.StringProperty() # format "tree_id.group_node_id" - - def pass_socket_data(self, inputs: bpy.types.NodeInputs, outputs: bpy.types.NodeOutputs): - """Should be used for passing data from/to group nodes to/from input/output nodes""" - for in_s, out_s in zip(inputs, outputs): - if out_s.identifier == '__extend__' or in_s.identifier == '__extend__': # virtual socket - break - out_s.sv_set(in_s.sv_get(deepcopy=False)) - - @extend_blender_class -class NodeGroupOutput(BaseInOutNodes, BaseNode): # todo copy node id problem - def process(self, group_node: SvGroupTreeNode): - self.pass_socket_data(self.inputs, group_node.outputs) +class NodeGroupOutput(BaseNode): # todo copy node id problem + def process(self): + return @extend_blender_class -class NodeGroupInput(BaseInOutNodes, BaseNode): - def process(self, group_node: SvGroupTreeNode): - self.pass_socket_data(group_node.inputs, self.outputs) +class NodeGroupInput(BaseNode): + def process(self): + return @extend_blender_class diff --git a/core/socket_data.py b/core/socket_data.py index 9809ec85a5..ce5aef1c13 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -100,6 +100,8 @@ def _update_sockets(self): for tree in BlTrees().sv_trees: for node in tree.nodes: for sock in chain(node.inputs, node.outputs): + if sock.bl_idname == 'NodeSocketVirtual': + continue if sock.socket_id in self._id_sock: ds = self._id_sock[sock.socket_id] debug(f"SOCKET ID DUPLICATION: " @@ -144,7 +146,7 @@ def _cut_text(text, max_size): socket_data_cache: dict[SockId, list] = dict() -# socket_data_cache = DebugMemory(socket_data_cache, False) +# socket_data_cache = DebugMemory(socket_data_cache) def sv_deep_copy(lst): diff --git a/core/simple_update_system.py b/core/update_system.py similarity index 90% rename from core/simple_update_system.py rename to core/update_system.py index e886b711a9..0edd691026 100644 --- a/core/simple_update_system.py +++ b/core/update_system.py @@ -55,17 +55,25 @@ def control_center(event: TreeEvent) -> Optional[Callable[[TreeEvent], Generator raise TypeError(f'Detected unknown event - {event}') # Add update tusk for the tree - return Tree.update + return Tree.main_update class SearchTree: + _from_nodes: dict['SvNode', set['SvNode']] + _to_nodes: dict['SvNode', set['SvNode']] + _from_sock: dict[NodeSocket, NodeSocket] + _sock_node: dict[NodeSocket, Node] + _links: set[tuple[NodeSocket, NodeSocket]] + def __init__(self, tree: NodeTree): self._tree = tree - self._from_nodes: dict[SvNode, set[SvNode]] = {n: set() for n in tree.nodes} - self._to_nodes: dict[SvNode, set[SvNode]] = {n: set() for n in tree.nodes} - self._from_sock: dict[NodeSocket, NodeSocket] = dict() # only connected - self._sock_node: dict[NodeSocket, Node] = dict() # only connected sockets - self._links: set[tuple[NodeSocket, NodeSocket]] = set() # from to socket + self._from_nodes = { + n: set() for n in tree.nodes if n.bl_idname != 'NodeFrame'} + self._to_nodes = { + n: set() for n in tree.nodes if n.bl_idname != 'NodeFrame'} + self._from_sock = dict() # only connected + self._sock_node = dict() # only connected sockets + self._links = set() # from to socket for link in (li for li in tree.links if not li.is_muted): self._from_nodes[link.to_node].add(link.from_node) @@ -163,19 +171,22 @@ def get(cls, tree: NodeTree) -> 'Tree': @profile(section="UPDATE") def update_animation(cls, event: TreeEvent): try: - g = cls.update(event, event.is_frame_changed, not event.is_animation_playing) + g = cls.main_update(event, event.is_frame_changed, not event.is_animation_playing) while True: next(g) except StopIteration: pass @classmethod - def update(cls, event: TreeEvent, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: + def main_update(cls, event: TreeEvent, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: + """Only for main trees""" + # print(f"UPDATE NODES {event.type=}, {event.tree.name=}") if update_nodes: tree = cls.get(event.tree) if not event.tree.sv_process and event.type in {event.TREE_UPDATE, event.NODES_UPDATE, event.SCENE_UPDATE}: - tree._outdated_nodes.update(event.updated_nodes) + if tree._outdated_nodes is not None: + tree._outdated_nodes.update(event.updated_nodes) return walker = tree._walk(list(event.updated_nodes or [])) @@ -202,12 +213,23 @@ def mark_outdated(cls, tree: NodeTree): if _tree := cls._tree_catch.get(tree.tree_id): _tree._is_updated = False + def update(self, updated_nodes: list['SvNode']): + walker = self._walk(list(updated_nodes or [])) + # walker = tree._debug_color(walker) + for node, prev_socks in walker: + with AddStatistic(node): + prepare_input_data(prev_socks, node.inputs) + node.process() + def __init__(self, tree: NodeTree): super().__init__(tree) self._tree_catch[tree.tree_id] = self self._is_updated = True # False if topology was changed self._outdated_nodes: Optional[set[SvNode]] = None # None means outdated all + # https://stackoverflow.com/a/68550238 + self._sort_nodes = lru_cache(maxsize=1)(self._sort_nodes) + def _walk(self, outdated: list['SvNode'] = None) -> tuple[Node, list[NodeSocket]]: # walk all nodes in the tree if self._outdated_nodes is None: @@ -228,9 +250,8 @@ def _walk(self, outdated: list['SvNode'] = None) -> tuple[Node, list[NodeSocket] else: node[UPDATE_KEY] = False - @lru_cache(maxsize=1) def _sort_nodes(self, outdated_nodes: frozenset['SvNode'] = None) -> list[tuple['SvNode', list[NodeSocket]]]: - + # print(f"Sort nodes {self._tree.name}") def node_walker(node_: 'SvNode'): for nn in self._to_nodes.get(node_, []): yield nn diff --git a/nodes/list_struct/slice.py b/nodes/list_struct/slice.py index ba7d349205..2e89efba3f 100644 --- a/nodes/list_struct/slice.py +++ b/nodes/list_struct/slice.py @@ -66,14 +66,14 @@ def process(self): if self.outputs['Slice'].is_linked: if self.level: - out = self.get(data, start, stop, self.level, self.slice) + out = self.get_(data, start, stop, self.level, self.slice) else: out = self.slice(data, start[0], stop[0]) self.outputs['Slice'].sv_set(out) if self.outputs['Other'].is_linked: if self.level: - out = self.get(data, start, stop, self.level, self.other) + out = self.get_(data, start, stop, self.level, self.other) else: out = self.other(data, start[0], stop[0]) self.outputs['Other'].sv_set(out) @@ -92,9 +92,9 @@ def other(self, data, start, stop): else: return None - def get(self, data, start, stop, level, f): + def get_(self, data, start, stop, level, f): if level > 1: # find level to work on - return [self.get(obj, start, stop, level - 1, f) for obj in data] + return [self.get_(obj, start, stop, level - 1, f) for obj in data] elif level == 1: # execute the chosen function data, start, stop = match_long_repeat([data, start, stop]) out = [] diff --git a/nodes/logic/evolver.py b/nodes/logic/evolver.py index 217dc58448..3a7d26f7c3 100644 --- a/nodes/logic/evolver.py +++ b/nodes/logic/evolver.py @@ -18,7 +18,7 @@ from bpy.props import ( BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty) -from sverchok.core.simple_update_system import Tree +from sverchok.core.update_system import Tree from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode from sverchok.utils.sv_operator_mixins import SvGenericNodeLocator diff --git a/nodes/logic/loop_out.py b/nodes/logic/loop_out.py index b094ce1f7b..217b66af5c 100644 --- a/nodes/logic/loop_out.py +++ b/nodes/logic/loop_out.py @@ -18,7 +18,7 @@ import bpy from bpy.props import EnumProperty -from sverchok.core.simple_update_system import Tree +from sverchok.core.update_system import Tree from sverchok.node_tree import SverchCustomTreeNode From ede5a4a0572e9d1d5298d3678523c84882bcf81a Mon Sep 17 00:00:00 2001 From: Durman Date: Fri, 27 May 2022 09:47:43 +0400 Subject: [PATCH 15/25] big update system refactoring: New module for tasks. Now tasks handler can handle multiple tasks TreeEvent class was split into multiple classes Update system keeps information about all changes between its execution --- core/__init__.py | 3 +- core/events.py | 67 ++++++++----- core/handlers.py | 4 +- core/main_tree_handler.py | 84 ++++------------ core/node_group.py | 4 +- core/tasks.py | 187 ++++++++++++++++++++++++++++++++++++ core/update_system.py | 195 +++++++++++++++++++++++++------------- node_tree.py | 35 ++----- 8 files changed, 391 insertions(+), 188 deletions(-) create mode 100644 core/tasks.py diff --git a/core/__init__.py b/core/__init__.py index e7994faf8d..9ec15150bd 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -13,7 +13,8 @@ "sv_custom_exceptions", "update_system", "sockets", "socket_data", "handlers", "main_tree_handler", - "events", "node_group", "group_handlers" + "events", "node_group", "group_handlers", + "tasks", ] def sv_register_modules(modules): diff --git a/core/events.py b/core/events.py index 62ff78eafc..0aa1c90486 100644 --- a/core/events.py +++ b/core/events.py @@ -17,18 +17,59 @@ from __future__ import annotations from collections.abc import Iterable -from enum import Enum, auto from typing import Union, List, TYPE_CHECKING from bpy.types import Node if TYPE_CHECKING: from sverchok.core.node_group import SvGroupTree, SvGroupTreeNode - from sverchok.node_tree import SverchCustomTreeNode, SverchCustomTree + from sverchok.node_tree import SverchCustomTreeNode, SverchCustomTree as SvTree SvNode = Union[SverchCustomTreeNode, SvGroupTreeNode, Node] class TreeEvent: + """Keeps information about what was changed during the even""" + # task should be run via timer only https://developer.blender.org/T82318#1053877 + tree: SvTree + + def __init__(self, tree): + self.tree = tree + + def __repr__(self): + return f"<{type(self).__name__} {self.tree.name=}>" + + +class ForceEvent(TreeEvent): + pass + + +class AnimationEvent(TreeEvent): + is_frame_changed: bool + is_animation_playing: bool + + def __init__(self, tree, is_frame_change, is_animation_laying): + super().__init__(tree) + self.is_frame_changed = is_frame_change + self.is_animation_playing = is_animation_laying + + +class SceneEvent(TreeEvent): + pass + + +class PropertyEvent(TreeEvent): + updated_nodes: Iterable[SvNode] + + def __init__(self, tree, updated_nodes): + super().__init__(tree) + self.updated_nodes = updated_nodes + + +class FileEvent: + pass + + +class _TreeEvent: # todo to remove TREE_UPDATE = 'tree_update' # some changed in a tree topology NODES_UPDATE = 'nodes_update' # changes in node properties, update animated nodes FORCE_UPDATE = 'force_update' # rebuild tree and reevaluate every node @@ -38,7 +79,7 @@ class TreeEvent: def __init__(self, event_type: str, - tree: SverchCustomTree, + tree: SvTree, updated_nodes: Iterable[SvNode] = None, cancel=True, is_frame_changed: bool = True, @@ -77,23 +118,3 @@ def tree(self) -> SvGroupTree: def __repr__(self): return f'{self.type.upper()} event, GROUP_NODE={self.group_node.name}, TREE={self.tree.name}' \ + (f', NODES={self.updated_nodes}' if self.updated_nodes else '') - - -class BlenderEventsTypes(Enum): - tree_update = auto() # this updates is calling last with exception of creating new node - node_update = auto() # it can be called last during creation new node event - add_node = auto() # it is called first in update wave - copy_node = auto() # it is called first in update wave - free_node = auto() # it is called first in update wave - add_link_to_node = auto() # it can detects only manually created links - node_property_update = auto() # can be in correct in current implementation - undo = auto() # changes in tree does not call any other update events - frame_change = auto() - - def print(self, updated_element=None): - event_name = f"EVENT: {self.name: <30}" - if updated_element is not None: - element_data = f"IN: {updated_element.bl_idname: <25} INSTANCE: {updated_element.name: <25}" - else: - element_data = "" - print(event_name + element_data) diff --git a/core/handlers.py b/core/handlers.py index 5caebccd4f..5e659876c6 100644 --- a/core/handlers.py +++ b/core/handlers.py @@ -3,7 +3,7 @@ from sverchok import old_nodes from sverchok import data_structure -from sverchok.core.events import TreeEvent +from sverchok.core.events import FileEvent from sverchok.core.socket_data import clear_all_socket_cache from sverchok.ui import bgl_callback_nodeview, bgl_callback_3dview from sverchok.utils import app_handler_ops @@ -159,7 +159,7 @@ def sv_pre_load(scene): sv_clean(scene) import sverchok.core.main_tree_handler as mh - mh.TreeHandler.send(TreeEvent(TreeEvent.FILE_RELOADED, tree=None)) + mh.TreeHandler.send(FileEvent()) @persistent diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index f8b3ac6e80..6cb6323ad7 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -16,8 +16,7 @@ import bpy from sverchok.core.sv_custom_exceptions import SvNoDataError, CancelError from sverchok.core.socket_conversions import ConversionPolicies -from sverchok.data_structure import post_load_call -from sverchok.core.events import TreeEvent +from sverchok.core.events import TreeEvent, SceneEvent from sverchok.utils.logging import debug, catch_log_error, log_error from sverchok.utils.tree_structure import Tree, Node from sverchok.utils.handle_blender_data import BlTrees, BlNode @@ -34,35 +33,21 @@ class TreeHandler: @staticmethod - def send(event: TreeEvent): - """Control center""" + def send(event): + """Main control center + 1. preprocess the event + 2. Pass the event to update system(s)""" # print(f"{event.type=}, {event.tree=}") - current_task = Task.get() - - # this should be first other wise other instructions can spoil the node statistic to redraw - if current_task and current_task.is_running(): - if event.cancel: - current_task.cancel() - else: - return # ignore the event # something changed in scene and it duplicates some tree events which should be ignored - elif event.type == TreeEvent.SCENE_UPDATE: - # Either the scene handler was triggered by changes in the tree or tree is still in progress - if current_task: - return # ignore the event - # this event was caused my update system itself and should be ignored - elif 'SKIP_UPDATE' in event.tree: + if isinstance(event, SceneEvent): + # this event was caused by update system itself and should be ignored + if 'SKIP_UPDATE' in event.tree: del event.tree['SKIP_UPDATE'] return - # force update - elif event.type == TreeEvent.FORCE_UPDATE: - event.tree['FORCE_UPDATE'] = True - # Add update tusk for the tree - if handler := sus.control_center(event): - Task.add(event, handler) + sus.control_center(event) @staticmethod def get_error_nodes(bl_tree) -> Iterator[Optional[Exception]]: @@ -133,23 +118,10 @@ def control_center(event: TreeEvent) -> bool: return add_tusk -def tree_event_loop(delay): - """Sverchok event handler""" - with catch_log_error(): - if task := Task.get(): - if not task.is_running(): - task.start() - task.run() # task should be run via timer only https://developer.blender.org/T82318#1053877 - return delay - - -tree_event_loop = partial(tree_event_loop, 0.01) - - class Task: _task: Optional['Task'] = None # for now running only one task is supported - __slots__ = ('_event', + __slots__ = ('event', '_handler_func', '_handler', '_node_tree_area', @@ -161,7 +133,7 @@ class Task: def add(cls, event: TreeEvent, handler: Callable) -> 'Task': if cls._task and cls._task.is_running(): raise RuntimeError(f"Can't update tree: {event.tree.name}," - f" already updating tree: {cls._task._event.tree.name}") + f" already updating tree: {cls._task.event.tree.name}") cls._task = cls(event, handler) return cls._task @@ -170,7 +142,7 @@ def get(cls) -> Optional['Task']: return cls._task def __init__(self, event, handler): - self._event: TreeEvent = event + self.event: TreeEvent = event self._handler_func: Callable[[TreeEvent], Generator] = handler self._handler: Optional[Generator[SvNode, None, None]] = None self._node_tree_area: Optional[bpy.types.Area] = None @@ -178,10 +150,10 @@ def __init__(self, event, handler): self._last_node: Optional[SvNode] = None def start(self): - changed_tree = self._event.tree + changed_tree = self.event.tree if self.is_running(): raise RuntimeError(f'Tree "{changed_tree.name}" already is being updated') - self._handler = self._handler_func(self._event) + self._handler = self._handler_func(self.event) # searching appropriate area index for reporting update progress for area in bpy.context.screen.areas: @@ -222,10 +194,10 @@ def cancel(self): def finish_task(self): try: # this only need to trigger scene changes handler again - if self._event.tree.nodes: - status = self._event.tree.nodes[-1].use_custom_color - self._event.tree.nodes[-1].use_custom_color = not status - self._event.tree.nodes[-1].use_custom_color = status + if self.event.tree.nodes: + status = self.event.tree.nodes[-1].use_custom_color + self.event.tree.nodes[-1].use_custom_color = not status + self.event.tree.nodes[-1].use_custom_color = status # this indicates that process of the tree is finished and next scene event can be skipped # the scene trigger will try to update all trees, so they all should be marked @@ -574,23 +546,3 @@ def handle_node_data(node: Node): out_sock.data = out_sock.bl_tween.sv_get() except SvNoDataError: pass - - -@post_load_call -def post_load_register(): - # when new file is loaded all timers are unregistered - # to make them persistent the post load handler should be used - # but it's also is possible that the timer was registered during registration of the add-on - if not bpy.app.timers.is_registered(tree_event_loop): - bpy.app.timers.register(tree_event_loop) - - -def register(): - """Registration of Sverchok event handler""" - # it appeared that the timers can be registered during the add-on initialization - # The timer should be registered here because post_load_register won't be called when an add-on is enabled by user - bpy.app.timers.register(tree_event_loop) - - -def unregister(): - bpy.app.timers.unregister(tree_event_loop) diff --git a/core/node_group.py b/core/node_group.py index 540e2d6c3e..21a1471010 100644 --- a/core/node_group.py +++ b/core/node_group.py @@ -401,7 +401,9 @@ def process(self): out_s.sv_set(in_s.sv_get(deepcopy=False)) tree = UpdateTree.get(self.node_tree) - tree.update([input_node]) + if tree.outdated_nodes is not None: + tree.outdated_nodes.add(input_node) + tree.update() for node in self.node_tree.nodes: if err := node.get(ERROR_KEY): diff --git a/core/tasks.py b/core/tasks.py new file mode 100644 index 0000000000..f6e816b9a5 --- /dev/null +++ b/core/tasks.py @@ -0,0 +1,187 @@ +import gc +from time import time +from functools import partial, cached_property, cache +from typing import TYPE_CHECKING, Optional, Generator + +import bpy +from sverchok.data_structure import post_load_call +from sverchok.core.sv_custom_exceptions import CancelError +from sverchok.utils.logging import catch_log_error, debug +from sverchok.utils.profile import profile +from sverchok.utils.handle_blender_data import BlTrees + +if TYPE_CHECKING: + from sverchok.node_tree import SverchCustomTree as SvTree + + +class Tasks: + """ + 1. Execute tasks + 2. Time the whole execution + 3. Display the progress in the UI + """ + _todo: set['Task'] + _current: Optional['Task'] + + def __init__(self): + self._todo = set() + self._current = None + + def __bool__(self): + return bool(self._current or self._todo) + + def add(self, task: 'Task'): + self._todo.add(task) + + @profile(section="UPDATE") + def run(self): + max_duration = 0.15 # 0.15 is max timer frequency + duration = 0 + + while self.current: + if duration > max_duration: + return + duration += self.current.run(max_duration-duration) + if self.current.last_node: + msg = f'Pres "ESC" to abort, updating node "{self.current.last_node.name}"' + self._report_progress(msg) + if self.current.is_exhausted: + self._next() + + self._finish() + + def cancel(self): + self._todo.clear() + if self._current: + try: + self._current.throw(CancelError) + except (StopIteration, RuntimeError): + pass + finally: # protection from the task to be stack forever + self._finish() + + @property + def current(self) -> Optional['Task']: + if self._current: + return self._current + elif self._todo: + self._start() + self._current = self._todo.pop() + return self._current + else: + return None + + def _start(self): + self._start_time + gc.disable() # for performance + + def _next(self): + self._report_progress() + self._current = self._todo.pop() if self._todo else None + del self._main_area + + def _finish(self): + self._report_progress() + + # this only need to trigger scene changes handler again + # todo should be proved that this is right location to call from + bpy.context.scene.update_tag() + + # this indicates that process of the tree is finished and next scene event can be skipped + # the scene trigger will try to update all trees, so they all should be marked + for t in BlTrees().sv_main_trees: + t['SKIP_UPDATE'] = True + + gc.enable() + debug(f'Global update - {int((time() - self._start_time) * 1000)}ms') + del self._start_time + + @cached_property + def _start_time(self): + return time() + + @cached_property + def _main_area(self) -> Optional: + """Searching appropriate area index for reporting update progress""" + if not self.current: + return + for area in bpy.context.screen.areas: + if area.ui_type == 'SverchCustomTreeType': + path = area.spaces[0].path + if path and path[-1].node_tree.name == self._current.tree.name: + return area + + def _report_progress(self, text: str = None): + if self._main_area: + self._main_area.header_text_set(text) + + +tasks = Tasks() + + +def tree_event_loop(delay): + """Sverchok tasks handler""" + with catch_log_error(): + if tasks: + tasks.run() + return delay + + +tree_event_loop = partial(tree_event_loop, 0.01) + + +class Task: + def __init__(self, tree, updater): + self.tree: SvTree = tree + self.is_exhausted = False + self.last_node = None + + self._updater: Generator = updater + self.__hash__ = cache(self.__hash__) + + def run(self, max_duration): + duration = 0 + try: + start_time = time() + while duration < max_duration: + self.last_node = next(self._updater) + duration = time() - start_time + return duration + + except StopIteration: + self.is_exhausted = True + return duration + + def throw(self, error): + self._updater.throw(error) + self.is_exhausted = True + + @property + def id(self): + return self.tree.tree_id + + def __eq__(self, other: 'Task'): + return self.tree.tree_id == other.tree.tree_id + + def __hash__(self): + return hash(self.tree.tree_id) + + +@post_load_call +def post_load_register(): + # when new file is loaded all timers are unregistered + # to make them persistent the post load handler should be used + # but it's also is possible that the timer was registered during registration of the add-on + if not bpy.app.timers.is_registered(tree_event_loop): + bpy.app.timers.register(tree_event_loop) + + +def register(): + """Registration of Sverchok event handler""" + # it appeared that the timers can be registered during the add-on initialization + # The timer should be registered here because post_load_register won't be called when an add-on is enabled by user + bpy.app.timers.register(tree_event_loop) + + +def unregister(): + bpy.app.timers.unregister(tree_event_loop) diff --git a/core/update_system.py b/core/update_system.py index 0edd691026..5951c303c0 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -1,15 +1,18 @@ from collections import defaultdict +from copy import copy from functools import lru_cache from graphlib import TopologicalSorter from time import perf_counter -from typing import TYPE_CHECKING, Optional, Generator, Callable, Iterable +from typing import TYPE_CHECKING, Optional, Generator, Iterable from bpy.types import Node, NodeSocket, NodeTree, NodeLink -from sverchok.core.events import TreeEvent +from sverchok.core.events import TreeEvent, AnimationEvent, SceneEvent, PropertyEvent, ForceEvent, FileEvent +import sverchok.core.tasks as ts from sverchok.core.socket_conversions import ConversionPolicies from sverchok.utils.profile import profile from sverchok.utils.logging import log_error from sverchok.utils.tree_walk import bfs_walk +from sverchok.utils.handle_blender_data import BlTrees if TYPE_CHECKING: from sverchok.node_tree import SverchCustomTreeNode as SvNode @@ -20,42 +23,53 @@ TIME_KEY = "US_time" -def control_center(event: TreeEvent) -> Optional[Callable[[TreeEvent], Generator]]: +def control_center(event: TreeEvent): + """ + 1. Update tree model lazily + 2. Check whether the event should be processed + 3. Process event or create task to process via timer""" + # frame update - # This event can't be handled via NodesUpdater during animation rendering because new frame change event - # can arrive before timer finishes its tusk. Or timer can start working before frame change is handled. - if event.type == TreeEvent.FRAME_CHANGE: - Tree.update_animation(event) - return - - # something changed in scene and it duplicates some tree events which should be ignored - elif event.type == TreeEvent.SCENE_UPDATE: - pass # todo similar to animation - # ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - - # mark given nodes as outdated - elif event.type == TreeEvent.NODES_UPDATE: - pass # todo add to outdated_nodes? - - # it will find changes in tree topology and mark related nodes as outdated - elif event.type == TreeEvent.TREE_UPDATE: - Tree.mark_outdated(event.tree) - - # force update - elif event.type == TreeEvent.FORCE_UPDATE: + # This event can't be handled via NodesUpdater during animation rendering + # because new frame change event can arrive before timer finishes its tusk. + # Or timer can start working before frame change is handled. + if isinstance(event, AnimationEvent): + if event.tree.sv_animate: + Tree.get(event.tree).is_animation_updated = False + Tree.update_animation(event) + + # something changed in the scene + elif isinstance(event, SceneEvent): + if event.tree.sv_scene_update and event.tree.sv_process: + Tree.get(event.tree).is_scene_updated = False + ts.tasks.add(ts.Task(event.tree, Tree.main_update(event.tree))) + + # nodes changed properties + elif isinstance(event, PropertyEvent): + tree = Tree.get(event.tree) + if tree.outdated_nodes is not None: + tree.outdated_nodes.update(event.updated_nodes) + if event.tree.sv_process: + ts.tasks.add(ts.Task(event.tree, Tree.main_update(event.tree))) + + # update the whole tree anyway + elif isinstance(event, ForceEvent): Tree.reset_tree(event.tree) + ts.tasks.add(ts.Task(event.tree, Tree.main_update(event.tree))) + + # mark that the tree topology has changed + elif isinstance(event, TreeEvent): + Tree.get(event.tree).is_updated = False + if event.tree.sv_process: + ts.tasks.add(ts.Task(event.tree, Tree.main_update(event.tree))) # new file opened - elif event.type == TreeEvent.FILE_RELOADED: + elif isinstance(event, FileEvent): Tree.reset_tree() - return # Unknown event else: - raise TypeError(f'Detected unknown event - {event}') - - # Add update tusk for the tree - return Tree.main_update + raise TypeError(f'Detected unknown {event=}') class SearchTree: @@ -147,6 +161,8 @@ def _remove_reroutes(self): self._from_nodes = {n: k for n, k in self._from_nodes.items() if n.bl_idname != 'NodeReroute'} self._to_nodes = {n: k for n, k in self._to_nodes.items() if n.bl_idname != 'NodeReroute'} + # todo add links between wifi nodes + class Tree(SearchTree): """It catches some data for more efficient searches compare to Blender @@ -154,43 +170,58 @@ class Tree(SearchTree): _tree_catch: dict[str, 'Tree'] = dict() # the module should be auto-reloaded to prevent crashes @classmethod - def get(cls, tree: NodeTree) -> 'Tree': + def get(cls, tree: NodeTree, refresh_tree=False) -> 'Tree': + """ + :refresh_tree: if True it will convert update flags into outdated + nodes. This can be expensive so it should be called only before tree + reevaluation + """ if tree.tree_id not in cls._tree_catch: _tree = cls(tree) else: _tree = cls._tree_catch[tree.tree_id] - if not _tree._is_updated: - old = _tree - _tree = cls(tree) - if old._outdated_nodes is not None: - _tree._outdated_nodes = old._outdated_nodes.copy() - _tree._outdated_nodes.update(_tree._update_difference(old)) + + if refresh_tree: + # update topology + if not _tree.is_updated: + old = _tree + _tree = old.copy() + + # update outdated nodes list + if _tree.outdated_nodes is not None: + if not _tree.is_updated: + _tree.outdated_nodes.update(_tree._update_difference(old)) + if not _tree.is_animation_updated: + _tree.outdated_nodes.update(_tree._animation_nodes()) + if not _tree.is_scene_updated: + _tree.outdated_nodes.update(_tree._scene_nodes()) + + _tree.is_updated = True + _tree.is_animation_updated = True + _tree.is_scene_updated = True + return _tree @classmethod @profile(section="UPDATE") - def update_animation(cls, event: TreeEvent): + def update_animation(cls, event: AnimationEvent): try: - g = cls.main_update(event, event.is_frame_changed, not event.is_animation_playing) + g = cls.main_update(event.tree, event.is_frame_changed, not event.is_animation_playing) while True: next(g) except StopIteration: pass @classmethod - def main_update(cls, event: TreeEvent, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: - """Only for main trees""" + def main_update(cls, tree: NodeTree, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: + """Only for main trees + 1. Whe it called the tree should have information of what is outdated""" + # todo add cancelling # print(f"UPDATE NODES {event.type=}, {event.tree.name=}") if update_nodes: - tree = cls.get(event.tree) - - if not event.tree.sv_process and event.type in {event.TREE_UPDATE, event.NODES_UPDATE, event.SCENE_UPDATE}: - if tree._outdated_nodes is not None: - tree._outdated_nodes.update(event.updated_nodes) - return - - walker = tree._walk(list(event.updated_nodes or [])) - # walker = tree._debug_color(walker) + up_tree = cls.get(tree, refresh_tree=True) + walker = up_tree._walk() + # walker = up_tree._debug_color(walker) for node, prev_socks in walker: with AddStatistic(node): yield node @@ -198,7 +229,7 @@ def main_update(cls, event: TreeEvent, update_nodes=True, update_interface=True) node.process() if update_interface: - update_ui(event.tree) + update_ui(tree) @classmethod def reset_tree(cls, tree: NodeTree = None): @@ -208,45 +239,75 @@ def reset_tree(cls, tree: NodeTree = None): else: cls._tree_catch.clear() - @classmethod - def mark_outdated(cls, tree: NodeTree): - if _tree := cls._tree_catch.get(tree.tree_id): - _tree._is_updated = False - - def update(self, updated_nodes: list['SvNode']): - walker = self._walk(list(updated_nodes or [])) + def update(self): + walker = self._walk() # walker = tree._debug_color(walker) for node, prev_socks in walker: with AddStatistic(node): prepare_input_data(prev_socks, node.inputs) node.process() + def copy(self) -> 'Tree': + """They copy will be with new topology if original tree was changed + since berth of the first tree. Other attributes copied as is.""" + copy_ = type(self)(self._tree) + for attr in self._copy_attrs: + setattr(copy_, attr, copy(getattr(self, attr))) + return copy_ + def __init__(self, tree: NodeTree): super().__init__(tree) self._tree_catch[tree.tree_id] = self - self._is_updated = True # False if topology was changed - self._outdated_nodes: Optional[set[SvNode]] = None # None means outdated all + + self.is_updated = True # False if topology was changed + self.is_animation_updated = True + self.is_scene_updated = True + self.outdated_nodes: Optional[set[SvNode]] = None # None means outdated all # https://stackoverflow.com/a/68550238 self._sort_nodes = lru_cache(maxsize=1)(self._sort_nodes) - def _walk(self, outdated: list['SvNode'] = None) -> tuple[Node, list[NodeSocket]]: + self._copy_attrs = [ + 'is_updated', + 'is_animation_updated', + 'is_scene_updated', + 'outdated_nodes', + ] + + def _animation_nodes(self) -> set['SvNode']: + an_nodes = set() + if not self.is_animation_updated: + for node in self._tree.nodes: + if getattr(node, 'is_animation_dependent', False) \ + and getattr(node, 'is_animatable', False): + an_nodes.add(node) + return an_nodes + + def _scene_nodes(self) -> set['SvNode']: + sc_nodes = set() + if not self.is_scene_updated: + for node in self._tree.nodes: + if getattr(node, 'is_scene_dependent', False) \ + and getattr(node, 'is_interactive', False): + sc_nodes.add(node) + return sc_nodes + + def _walk(self) -> tuple[Node, list[NodeSocket]]: # walk all nodes in the tree - if self._outdated_nodes is None: + if self.outdated_nodes is None: outdated = None - self._outdated_nodes = set() + self.outdated_nodes = set() # walk triggered nodes and error nodes from previous updates else: - outdated.extend(self._outdated_nodes) - outdated = frozenset(outdated) - self._outdated_nodes.clear() + outdated = frozenset(self.outdated_nodes) + self.outdated_nodes.clear() # todo what if execution was canceled? for node, other_socks in self._sort_nodes(outdated): # execute node only if all previous nodes are updated if all(n.get(UPDATE_KEY, True) for sock in other_socks if (n := self._sock_node.get(sock))): yield node, other_socks if node.get(ERROR_KEY, False): - self._outdated_nodes.add(node) + self.outdated_nodes.add(node) else: node[UPDATE_KEY] = False diff --git a/node_tree.py b/node_tree.py index 9984ac7d5a..075e2dcc57 100644 --- a/node_tree.py +++ b/node_tree.py @@ -14,7 +14,7 @@ from bpy.types import NodeTree from sverchok.core.sv_custom_exceptions import SvNoDataError -from sverchok.core.events import TreeEvent +from sverchok.core.events import TreeEvent, ForceEvent, PropertyEvent, SceneEvent, AnimationEvent from sverchok.core.main_tree_handler import TreeHandler from sverchok.data_structure import classproperty, post_load_call from sverchok.utils import get_node_class_reference @@ -111,7 +111,7 @@ def on_draft_mode_changed(self, context): name="Process", default=True, description='Update upon tree and node property changes', - update=lambda s, c: TreeHandler.send(TreeEvent(TreeEvent.TREE_UPDATE, s)), + update=lambda s, c: TreeHandler.send(TreeEvent(s)), options=set(), ) sv_animate: BoolProperty(name="Animate", default=True, description='Animate this layout', options=set()) @@ -144,50 +144,29 @@ def on_draft_mode_changed(self, context): def update(self): """This method is called if collection of nodes or links of the tree was changed""" - TreeHandler.send(TreeEvent(TreeEvent.TREE_UPDATE, self)) + TreeHandler.send(TreeEvent(self)) def force_update(self): """Update whole tree from scratch""" # ideally we would never like to use this method but we live in the real world - TreeHandler.send(TreeEvent(TreeEvent.FORCE_UPDATE, self)) + TreeHandler.send(ForceEvent(self)) def update_nodes(self, nodes, cancel=True): """This method expects to get list of its nodes which should be updated""" - return TreeHandler.send(TreeEvent(TreeEvent.NODES_UPDATE, self, nodes, cancel)) + return TreeHandler.send(PropertyEvent(self, nodes)) def scene_update(self): """This method should be called by scene changes handler it ignores events related with S sverchok trees in other cases it updates nodes which read data from Blender""" - def nodes_to_update(): - for node in self.nodes: - try: - if node.is_scene_dependent and node.is_interactive: - yield node - except AttributeError: - pass - if self.sv_scene_update: - TreeHandler.send(TreeEvent(TreeEvent.SCENE_UPDATE, self, nodes_to_update(), cancel=False)) + TreeHandler.send(SceneEvent(self)) def process_ani(self, frame_changed: bool, animation_playing: bool): """ Process the Sverchok node tree if animation layers show true. For animation callback/handler """ - def animated_nodes(): - for node in self.nodes: - try: - if node.is_animation_dependent and node.is_animatable: - yield node - except AttributeError: - pass - if self.sv_animate: - TreeHandler.send(TreeEvent( - TreeEvent.FRAME_CHANGE, - self, - animated_nodes(), - is_frame_changed=frame_changed, - is_animation_playing=animation_playing)) + TreeHandler.send(AnimationEvent(self, frame_changed, animation_playing)) def update_ui(self, nodes_errors, update_time): """ The method get information about node statistic of last update from the handler to show in view space From 7b4d914c61a6db644154f7da9499df750b00b1b0 Mon Sep 17 00:00:00 2001 From: Durman Date: Sun, 29 May 2022 18:55:32 +0400 Subject: [PATCH 16/25] add updates during node groups editing to new update system --- core/__init__.py | 3 +- core/events.py | 73 ++++-------- core/group_handlers.py | 213 ------------------------------------ core/group_update_system.py | 192 ++++++++++++++++++++++++++++++++ core/handlers.py | 4 +- core/main_tree_handler.py | 73 ++++-------- core/node_group.py | 103 +++++------------ core/socket_data.py | 2 +- core/tasks.py | 4 + core/update_system.py | 127 +++++++++------------ node_tree.py | 39 ++++--- nodes/logic/evolver.py | 6 +- nodes/logic/loop_out.py | 6 +- utils/geom.py | 2 +- utils/tree_walk.py | 16 ++- 15 files changed, 371 insertions(+), 492 deletions(-) delete mode 100644 core/group_handlers.py create mode 100644 core/group_update_system.py diff --git a/core/__init__.py b/core/__init__.py index 9ec15150bd..0657eeb5a5 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -13,8 +13,9 @@ "sv_custom_exceptions", "update_system", "sockets", "socket_data", "handlers", "main_tree_handler", - "events", "node_group", "group_handlers", + "events", "node_group", "tasks", + "group_update_system", ] def sv_register_modules(modules): diff --git a/core/events.py b/core/events.py index 0aa1c90486..928017d718 100644 --- a/core/events.py +++ b/core/events.py @@ -17,14 +17,15 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Union, List, TYPE_CHECKING +from typing import Union, TYPE_CHECKING from bpy.types import Node if TYPE_CHECKING: - from sverchok.core.node_group import SvGroupTree, SvGroupTreeNode + from sverchok.core.node_group import SvGroupTree as GrTree, \ + SvGroupTreeNode as GrNode from sverchok.node_tree import SverchCustomTreeNode, SverchCustomTree as SvTree - SvNode = Union[SverchCustomTreeNode, SvGroupTreeNode, Node] + SvNode = Union[SverchCustomTreeNode, GrNode, Node] class TreeEvent: @@ -65,56 +66,26 @@ def __init__(self, tree, updated_nodes): self.updated_nodes = updated_nodes -class FileEvent: - pass +class GroupTreeEvent(TreeEvent): + tree: GrTree + update_path: list[GrNode] + def __init__(self, tree, update_path): + super().__init__(tree) + self.update_path = update_path -class _TreeEvent: # todo to remove - TREE_UPDATE = 'tree_update' # some changed in a tree topology - NODES_UPDATE = 'nodes_update' # changes in node properties, update animated nodes - FORCE_UPDATE = 'force_update' # rebuild tree and reevaluate every node - FRAME_CHANGE = 'frame_change' # unlike other updates this one should be un-cancellable - SCENE_UPDATE = 'scene_update' # something was changed in the scene - FILE_RELOADED = 'file_reloaded' # New files was opened - - def __init__(self, - event_type: str, - tree: SvTree, - updated_nodes: Iterable[SvNode] = None, - cancel=True, - is_frame_changed: bool = True, - is_animation_playing: bool = False): - self.type = event_type - self.tree = tree - self.updated_nodes = updated_nodes - self.cancel = cancel - self.is_frame_changed = is_frame_changed - self.is_animation_playing = is_animation_playing - def __repr__(self): - return f"" - - -class GroupEvent: - GROUP_NODE_UPDATE = 'group_node_update' - GROUP_TREE_UPDATE = 'group_tree_update' - NODES_UPDATE = 'nodes_update' - EDIT_GROUP_NODE = 'edit_group_node' # upon pressing edit button or Tab - - def __init__(self, - event_type: str, - group_nodes_path: List[SvGroupTreeNode], - updated_nodes: List[SvNode] = None): - self.type = event_type - self.group_node = group_nodes_path[-1] - self.group_nodes_path = group_nodes_path - self.updated_nodes = updated_nodes - self.to_update = group_nodes_path[-1].is_active +class GroupPropertyEvent(GroupTreeEvent): + updated_nodes: Iterable[SvNode] - @property - def tree(self) -> SvGroupTree: - return self.group_node.node_tree + def __init__(self, tree, update_path, update_nodes): + super().__init__(tree, update_path) + self.updated_nodes = update_nodes - def __repr__(self): - return f'{self.type.upper()} event, GROUP_NODE={self.group_node.name}, TREE={self.tree.name}' \ - + (f', NODES={self.updated_nodes}' if self.updated_nodes else '') + +class FileEvent: + pass + + +class TreesGraphEvent: + pass diff --git a/core/group_handlers.py b/core/group_handlers.py deleted file mode 100644 index 3961463604..0000000000 --- a/core/group_handlers.py +++ /dev/null @@ -1,213 +0,0 @@ -# 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 - -""" -Purpose of this module is calling `process` methods of nodes in appropriate order according their relations in a tree -and keeping `updating` statistics. -""" - -from __future__ import annotations - -from time import time -from typing import Generator, TYPE_CHECKING, Union, List, Optional, Iterator, Tuple - -from sverchok.core.events import GroupEvent -from sverchok.core.main_tree_handler import empty_updater, Task, ContextTrees, handle_node_data, PathManager -from sverchok.core.sv_custom_exceptions import CancelError -from sverchok.utils.tree_structure import Node -from sverchok.utils.logging import log_error -from sverchok.utils.handle_blender_data import BlNode - -if TYPE_CHECKING: - from sverchok.core.node_group import SvGroupTree, SvGroupTreeNode - from sverchok.node_tree import SverchCustomTree, SverchCustomTreeNode - SvTree = Union[SvGroupTree, SverchCustomTree] - SvNode = Union[SverchCustomTreeNode, SvGroupTreeNode] - - -class MainHandler: - @classmethod - def update(cls, event: GroupEvent, trees_ui_to_update: set) -> Iterator[Node]: - """ - This method should be called by group nodes for updating their tree - Also it means that input data was changed - """ - ContextTrees.mark_nodes_outdated( - event.tree, event.updated_nodes, PathManager.generate_path(event.group_nodes_path)) - return group_tree_handler(event.group_nodes_path, trees_ui_to_update) - - @classmethod - def send(cls, event: GroupEvent): - current_task = Task.get() - # this should be first other wise other instructions can spoil the node statistic to redraw - if current_task and current_task.is_running(): - current_task.cancel() - - # mark given nodes as outdated - if event.type == GroupEvent.NODES_UPDATE: - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - - # it will find (before the tree evaluation) changes in tree topology and mark related nodes as outdated - elif event.type == GroupEvent.GROUP_TREE_UPDATE: - ContextTrees.mark_tree_outdated(event.tree) - - # trigger just to evaluate debug nodes and update tree ui - elif event.type == GroupEvent.EDIT_GROUP_NODE: - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - - elif event.type == GroupEvent.GROUP_NODE_UPDATE: - raise TypeError(f'"Group node update" event should use update method instead of send') - - # Unknown event - else: - raise TypeError(f'Detected unknown event - {event}') - - # Add update tusk for the tree - if event.to_update: - Task.add(event) - - @staticmethod - def get_error_nodes(group_nodes_path: List[SvGroupTreeNode]) -> Iterator[Optional[Exception]]: - """Returns error if a node has error during execution or None""" - path = PathManager.generate_path(group_nodes_path) - tree = ContextTrees.get(group_nodes_path[-1].node_tree, rebuild=False) - for node in group_nodes_path[-1].node_tree.nodes: - if node.bl_idname in {'NodeReroute', 'NodeFrame'}: - yield None - continue - with tree.set_exec_context(path): - error = tree.nodes[node.name].error - yield error - - @staticmethod - def get_nodes_update_time(group_nodes_path: List[SvGroupTreeNode]) -> Iterator[Optional[float]]: - """Returns duration of a node being executed in milliseconds or None if there was an error""" - path = PathManager.generate_path(group_nodes_path) - tree = ContextTrees.get(group_nodes_path[-1].node_tree, rebuild=False) - for node in group_nodes_path[-1].node_tree.nodes: - if node.bl_idname in {'NodeReroute', 'NodeFrame'}: - yield None - continue - with tree.set_exec_context(path): - upd_time = tree.nodes[node.name].update_time - yield upd_time - - @staticmethod - def get_cum_time(group_nodes_path: List[SvGroupTreeNode]) -> Iterator[Optional[float]]: - bl_tree = group_nodes_path[-1].node_tree - cum_time_nodes = ContextTrees.calc_cam_update_time_group(bl_tree, group_nodes_path) - for node in group_nodes_path[-1].node_tree.nodes: - yield cum_time_nodes.get(node) - - -def group_tree_handler(group_nodes_path: List[SvGroupTreeNode], trees_ui_to_update: set)\ - -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: - group_node = group_nodes_path[-1] - path = PathManager.generate_path(group_nodes_path) - tree = ContextTrees.get(group_node.node_tree, path) - is_debug_to_update = group_node.node_tree in trees_ui_to_update \ - and group_node.node_tree.group_node_name == group_node.name - - out_nodes = [n for n in tree.nodes if BlNode(n.bl_tween).is_debug_node] - out_nodes.extend([tree.nodes.active_output] if tree.nodes.active_output else []) - - # output - output_was_changed = False - node_error = None - - with tree.set_exec_context(path): - for node in tree.sorted_walk(out_nodes): - can_be_updated = all(n.is_updated for n in node.last_nodes) - if not can_be_updated: - # here different logic can be implemented but for this we have to know if is there any output of the node - # we could leave the node as updated and don't broke work of the rest forward nodes - # but if the node does not have any output all next nodes will gen NoDataError what is horrible - node.is_updated = False - node.is_output_changed = False - continue - - # update node with sub update system - if hasattr(node.bl_tween, 'updater'): - sub_updater = group_node_updater(node, group_nodes_path) - # regular nodes - elif hasattr(node.bl_tween, 'process'): - sub_updater = node_updater(node, group_node, is_debug_to_update) - # reroutes - else: - node.is_updated = True - sub_updater = empty_updater(it_output_changed=True, node_error=None) - - start_time = time() - node_error = yield from sub_updater - update_time = time() - start_time - - if node.is_output_changed or node_error: - node.error = node_error - node.update_time = None if node_error else update_time - - if node.is_output_changed and node.bl_tween.bl_idname == 'NodeGroupOutput': - output_was_changed = True - - return output_was_changed, node_error - - -def group_node_updater(node: Node, group_nodes_path=None) -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: - """The node should have updater attribute""" - previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) - should_be_updated = (not node.is_updated or node.is_input_changed or previous_nodes_are_changed) - yield node # yield groups node so it be colored by node Updater if necessary - updater = node.bl_tween.updater(group_nodes_path=group_nodes_path, is_input_changed=should_be_updated) - with handle_node_data(node): - is_output_changed, out_error = yield from updater - node.is_input_changed = False - node.is_updated = not out_error - node.is_output_changed = is_output_changed - return out_error - - -def node_updater(node: Node, group_node: SvGroupTreeNode, is_debug_to_update: bool): - """ - Group tree should have proper node_ids before calling this method - Also this method will mark next nodes as outdated for current context - """ - if BlNode(node.bl_tween).is_debug_node and not is_debug_to_update: - return None, None # Early exit otherwise it will spoil node statuses - - node_error = None - - previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) - should_be_updated = not node.is_updated or node.is_input_changed or previous_nodes_are_changed - - node.is_output_changed = False # it should always False unless the process method was called - node.is_input_changed = False # if node wont be able to handle new input it will be seen in its update status - if should_be_updated: - try: - with handle_node_data(node): - if node.bl_tween.bl_idname in {'NodeGroupInput', 'NodeGroupOutput'}: - node.bl_tween.process(group_node) - else: - yield node # yield only normal nodes - node.bl_tween.process() - node.is_updated = True - node.is_output_changed = True - - # is_output_changed of a node without context is not reliable - # if the tree presented multiple times attribute will be true only in first execution - # so this should let to know next nodes that they also should be updated - if not node.is_input_linked: - for next_n in node.next_nodes: - if next_n.is_input_linked or BlNode(next_n.bl_tween).is_debug_node: - del next_n.is_input_changed - - except CancelError as e: - node.is_updated = False - node_error = e - except Exception as e: - node.is_updated = False - node_error = e - log_error(e) - return node_error diff --git a/core/group_update_system.py b/core/group_update_system.py new file mode 100644 index 0000000000..97d6afa2ba --- /dev/null +++ b/core/group_update_system.py @@ -0,0 +1,192 @@ +from collections import defaultdict +from typing import TYPE_CHECKING, overload, Iterator, Callable + +from bpy.types import NodeTree, Node, NodeSocket +import sverchok.core.update_system as us +import sverchok.core.events as ev +import sverchok.core.tasks as ts +from sverchok.utils.handle_blender_data import BlTrees +from sverchok.utils.tree_walk import recursion_dfs_walk + +if TYPE_CHECKING: + from sverchok.node_tree import (SverchCustomTreeNode as SvNode, + SverchCustomTree as SvTree) + from sverchok.core.node_group import SvGroupTree as GrTree, \ + SvGroupTreeNode as GrNode + + +def control_center(event): + """ + 1. Update tree model lazily + 2. Check whether the event should be processed + 3. Process event or create task to process via timer""" + was_executed = False + + # property of some node of a group tree was changed + if type(event) is ev.GroupPropertyEvent: + was_executed = True + gr_tree = GroupUpdateTree.get(event.tree) + gr_tree.add_outdated(event.updated_nodes) + gr_tree.update_path = event.update_path + for main_tree in trees_graph[event.tree]: + us.UpdateTree.get(main_tree).add_outdated(trees_graph[main_tree, event.tree]) + if main_tree.sv_process: + ts.tasks.add(ts.Task(main_tree, us.UpdateTree.main_update(main_tree))) + + # topology of a group tree was changed + elif type(event) is ev.GroupTreeEvent: + was_executed = True + gr_tree = GroupUpdateTree.get(event.tree) + gr_tree.is_updated = False + # gr_tree.update_path = event.update_path + for main_tree in trees_graph[event.tree]: + us.UpdateTree.get(main_tree).add_outdated(trees_graph[main_tree, event.tree]) + if main_tree.sv_process: + ts.tasks.add(ts.Task(main_tree, us.UpdateTree.main_update(main_tree))) + + # Connections between trees were changed + elif type(event) is ev.TreesGraphEvent: + was_executed = True + trees_graph.is_updated = False + + return was_executed + + +class GroupUpdateTree(us.UpdateTree): + get: Callable[['GrTree'], 'GroupUpdateTree'] + + def update(self, node: 'GrNode'): + self._exec_path.append(node) + try: + is_opened_tree = self.update_path == self._exec_path + if not is_opened_tree: + self._viewer_nodes = {node.active_output()} + + walker = self._walk() + walker = self._debug_color(walker) + for node, prev_socks in walker: + with us.AddStatistic(node): + us.prepare_input_data(prev_socks, node.inputs) + node.process() + + if is_opened_tree: + us.update_ui(self._tree) + + except Exception: + raise + finally: + self._exec_path.pop() + + def __init__(self, tree): + super().__init__(tree) + # update UI for the tree opened under the given path + self.update_path: list['GrNode'] = [] + + self._exec_path: list['GrNode'] = [] + + # if not presented all output nodes will be updated + self._viewer_nodes: set[Node] = set() # not presented in main trees yet + + self._copy_attrs.extend(['_exec_path', 'update_path', '_viewer_nodes']) + + def _walk(self) -> tuple[Node, list[NodeSocket]]: + # walk all nodes in the tree + if self._outdated_nodes is None: + outdated = None + viewers = None + self._outdated_nodes = set() + self._viewer_nodes = set() + # walk triggered nodes and error nodes from previous updates + else: + outdated = frozenset(self._outdated_nodes) + viewers = frozenset(self._viewer_nodes) + self._outdated_nodes.clear() # todo what if execution was canceled? + self._viewer_nodes.clear() + + for node, other_socks in self._sort_nodes(outdated, viewers): + # execute node only if all previous nodes are updated + if all(n.get(us.UPDATE_KEY, True) for sock in other_socks if (n := self._sock_node.get(sock))): + yield node, other_socks + if node.get(us.ERROR_KEY, False): + self._outdated_nodes.add(node) + else: + node[us.UPDATE_KEY] = False + + +class TreesGraph: + _group_main: dict['GrTree', set['SvTree']] + _entry_nodes: dict['SvTree', dict['GrTree', set['SvNode']]] + + def __init__(self): + self.is_updated = False + + self._group_main = defaultdict(set) + self._entry_nodes = defaultdict(lambda: defaultdict(set)) + + @overload + def __getitem__(self, item: 'GrTree') -> set['SvTree']: ... + @overload + def __getitem__(self, item: tuple['SvTree', 'GrTree']) -> set['SvNode']: ... + + def __getitem__(self, item): + # print(self) + if not self.is_updated: + self._update() + if isinstance(item, tuple): + sv_tree, gr_tree = item + return self._entry_nodes[sv_tree][gr_tree] + else: + return self._group_main[item] + + def _update(self): + # print("REFRESH TreesGraph") + self._group_main.clear() + self._entry_nodes.clear() + for tree in BlTrees().sv_main_trees: + for gr_tree, gr_node in self._walk(tree): + self._group_main[gr_tree].add(tree) + self._entry_nodes[tree][gr_tree].add(gr_node) + self.is_updated = True + + @staticmethod + def _walk(from_: NodeTree) -> Iterator[tuple[NodeTree, 'SvNode']]: + current_entry_node = None + + def next_(_tree): + nonlocal current_entry_node + for node in _tree.nodes: + if node.bl_idname == 'SvGroupTreeNode' and node.node_tree: + if _tree.bl_idname == 'SverchCustomTreeType': + current_entry_node = node + yield node.node_tree + + walker = recursion_dfs_walk([from_], next_) + next(walker) # ignore first itself tree + for tree in walker: + yield tree, current_entry_node + + def __repr__(self): + def group_main_str(): + for gr_tree, trees in self._group_main.items(): + yield f" {gr_tree.name}:" + for tree in trees: + yield f" {tree.name}" + + def entry_nodes_str(): + for tree, groups in self._entry_nodes.items(): + yield f" {tree.name}:" + for group, nodes in groups.items(): + yield f" {group.name}:" + for node in nodes: + yield f" {node.name}" + + gm = "\n".join(group_main_str()) + en = "\n".join(entry_nodes_str()) + str_ = f"" + return str_ + + +trees_graph = TreesGraph() diff --git a/core/handlers.py b/core/handlers.py index 5e659876c6..ad9e5fc1c2 100644 --- a/core/handlers.py +++ b/core/handlers.py @@ -3,7 +3,7 @@ from sverchok import old_nodes from sverchok import data_structure -from sverchok.core.events import FileEvent +import sverchok.core.events as ev from sverchok.core.socket_data import clear_all_socket_cache from sverchok.ui import bgl_callback_nodeview, bgl_callback_3dview from sverchok.utils import app_handler_ops @@ -159,7 +159,7 @@ def sv_pre_load(scene): sv_clean(scene) import sverchok.core.main_tree_handler as mh - mh.TreeHandler.send(FileEvent()) + mh.TreeHandler.send(ev.FileEvent()) @persistent diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index 6cb6323ad7..8338b9d4d6 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -9,19 +9,19 @@ import gc from contextlib import contextmanager -from functools import partial from time import time from typing import Dict, Generator, Optional, Iterator, Tuple, NewType, List, TYPE_CHECKING, Callable import bpy from sverchok.core.sv_custom_exceptions import SvNoDataError, CancelError from sverchok.core.socket_conversions import ConversionPolicies -from sverchok.core.events import TreeEvent, SceneEvent -from sverchok.utils.logging import debug, catch_log_error, log_error +import sverchok.core.events as ev +from sverchok.utils.logging import debug, log_error from sverchok.utils.tree_structure import Tree, Node from sverchok.utils.handle_blender_data import BlTrees, BlNode from sverchok.utils.profile import profile -import sverchok.core.update_system as sus +import sverchok.core.update_system as us +import sverchok.core.group_update_system as gus if TYPE_CHECKING: from sverchok.core.node_group import SvGroupTreeNode as SvNode @@ -29,6 +29,8 @@ Path = NewType('Path', str) # concatenation of group node ids +update_systems = [us.control_center, gus.control_center] + class TreeHandler: @@ -37,17 +39,26 @@ def send(event): """Main control center 1. preprocess the event 2. Pass the event to update system(s)""" - # print(f"{event.type=}, {event.tree=}") + # print(f"{event=}") # something changed in scene and it duplicates some tree events which should be ignored - if isinstance(event, SceneEvent): + if isinstance(event, ev.SceneEvent): # this event was caused by update system itself and should be ignored if 'SKIP_UPDATE' in event.tree: del event.tree['SKIP_UPDATE'] return + was_handled = dict() # Add update tusk for the tree - sus.control_center(event) + for handler in update_systems: + res = handler(event) + was_handled[handler] = res + + if (results := sum(was_handled.values())) > 1: + duplicates = [f.__module__ for f, r in was_handled if r == 1] + raise RuntimeError(f"{event=} was executed more than one time, {duplicates=}") + elif results == 0: + raise RuntimeError(f"{event} was not handled") @staticmethod def get_error_nodes(bl_tree) -> Iterator[Optional[Exception]]: @@ -80,44 +91,6 @@ def get_cum_time(bl_tree) -> Iterator[Optional[float]]: yield cum_time_nodes.get(node) -def control_center(event: TreeEvent) -> bool: - add_tusk = True - - # something changed in scene and it duplicates some tree events which should be ignored - if event.type == TreeEvent.SCENE_UPDATE: - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - - # frame update - # This event can't be handled via NodesUpdater during animation rendering because new frame change event - # can arrive before timer finishes its tusk. Or timer can start working before frame change is handled. - elif event.type == TreeEvent.FRAME_CHANGE: - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - profile(section="UPDATE")(lambda: list(global_updater(event.type)))() - add_tusk = False - - # mark given nodes as outdated - elif event.type == TreeEvent.NODES_UPDATE: - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - - # it will find changes in tree topology and mark related nodes as outdated - elif event.type == TreeEvent.TREE_UPDATE: - ContextTrees.mark_tree_outdated(event.tree) - - # force update - elif event.type == TreeEvent.FORCE_UPDATE: - ContextTrees.reset_data(event.tree) - - # new file opened - elif event.type == TreeEvent.FILE_RELOADED: - ContextTrees.reset_data() - - # Unknown event - else: - raise TypeError(f'Detected unknown event - {event}') - - return add_tusk - - class Task: _task: Optional['Task'] = None # for now running only one task is supported @@ -130,7 +103,7 @@ class Task: ) @classmethod - def add(cls, event: TreeEvent, handler: Callable) -> 'Task': + def add(cls, event: ev.TreeEvent, handler: Callable) -> 'Task': if cls._task and cls._task.is_running(): raise RuntimeError(f"Can't update tree: {event.tree.name}," f" already updating tree: {cls._task.event.tree.name}") @@ -142,8 +115,8 @@ def get(cls) -> Optional['Task']: return cls._task def __init__(self, event, handler): - self.event: TreeEvent = event - self._handler_func: Callable[[TreeEvent], Generator] = handler + self.event: ev.TreeEvent = event + self._handler_func: Callable[[ev.TreeEvent], Generator] = handler self._handler: Optional[Generator[SvNode, None, None]] = None self._node_tree_area: Optional[bpy.types.Area] = None self._start_time: Optional[float] = None @@ -234,12 +207,12 @@ def global_updater(event_type: str) -> Generator[Node, None, None]: for bl_tree in BlTrees().sv_main_trees: was_changed = False # update only trees which should be animated (for performance improvement in case of many trees) - if event_type == TreeEvent.FRAME_CHANGE: + if event_type == ev.TreeEvent.FRAME_CHANGE: if bl_tree.sv_animate: was_changed = yield from tree_updater(bl_tree, trees_ui_to_update) # tree should be updated any way - elif event_type == TreeEvent.FORCE_UPDATE and 'FORCE_UPDATE' in bl_tree: + elif event_type == ev.TreeEvent.FORCE_UPDATE and 'FORCE_UPDATE' in bl_tree: del bl_tree['FORCE_UPDATE'] was_changed = yield from tree_updater(bl_tree, trees_ui_to_update) diff --git a/core/node_group.py b/core/node_group.py index 21a1471010..c689e8f89c 100644 --- a/core/node_group.py +++ b/core/node_group.py @@ -9,22 +9,22 @@ import time from collections import namedtuple from functools import reduce -from itertools import cycle -from typing import Tuple, List, Set, Dict, Iterator, Generator, Optional +from typing import Tuple, List, Set, Dict, Iterator, Optional import bpy -from sverchok.core.main_tree_handler import empty_updater +from bpy.props import BoolProperty +from sverchok.core.main_tree_handler import TreeHandler from sverchok.data_structure import extend_blender_class from mathutils import Vector -from sverchok.core.group_handlers import MainHandler from sverchok.core.sockets import socket_type_names -from sverchok.core.events import GroupEvent -from sverchok.core.update_system import ERROR_KEY, Tree as UpdateTree -from sverchok.utils.tree_structure import Tree, Node +import sverchok.core.events as ev +import sverchok.core.group_update_system as gus +from sverchok.core.update_system import ERROR_KEY +from sverchok.utils.tree_structure import Tree from sverchok.utils.sv_node_utils import recursive_framed_location_finder -from sverchok.utils.handle_blender_data import BlNode, BlTrees -from sverchok.node_tree import UpdateNodes, SvNodeTreeCommon, SverchCustomTreeNode +from sverchok.utils.handle_blender_data import BlTrees +from sverchok.node_tree import SvNodeTreeCommon, SverchCustomTreeNode class SvGroupTree(SvNodeTreeCommon, bpy.types.NodeTree): @@ -33,14 +33,14 @@ class SvGroupTree(SvNodeTreeCommon, bpy.types.NodeTree): bl_icon = 'NODETREE' bl_label = 'Group tree' - handler = MainHandler - sv_process = True # for consistency with main tree + handler = TreeHandler # should be updated by "Go to edit group tree" operator group_node_name: bpy.props.StringProperty(options={'SKIP_SAVE'}) # Always False, does not have sense to have for nested trees, sine of draft mode refactoring sv_draft: bpy.props.BoolProperty(options={'SKIP_SAVE'}) + sv_show_time_nodes: BoolProperty(default=False, options={'SKIP_SAVE'}) @classmethod def poll(cls, context): @@ -116,7 +116,7 @@ def update(self): self.check_reroutes_sockets() self.update_sockets() # probably more precise trigger could be found for calling this method - self.handler.send(GroupEvent(GroupEvent.GROUP_TREE_UPDATE, self.get_update_path())) + self.handler.send(ev.GroupTreeEvent(self, self.get_update_path())) def update_sockets(self): # todo it lets simplify sockets API """Set properties of sockets of parent nodes and of output modes""" @@ -205,7 +205,7 @@ def update_nodes(self, nodes: list): if not self.group_node_name: # initialization tree return - self.handler.send(GroupEvent(GroupEvent.NODES_UPDATE, self.get_update_path(), updated_nodes=nodes)) + self.handler.send(ev.GroupPropertyEvent(self, self.get_update_path(), nodes)) def parent_nodes(self) -> Iterator['SvGroupTreeNode']: """Returns all parent nodes""" @@ -215,20 +215,6 @@ def parent_nodes(self) -> Iterator['SvGroupTreeNode']: if hasattr(node, 'node_tree') and node.node_tree and node.node_tree.name == self.name: yield node - def update_ui(self, group_nodes_path: List['SvGroupTreeNode']): - """updating tree contextual information -> node colors, objects number in sockets""" - nodes_errors = self.handler.get_error_nodes(group_nodes_path) - to_show_update_time = group_nodes_path[0].id_data.sv_show_time_nodes - time_mode = group_nodes_path[0].id_data.show_time_mode - if to_show_update_time: - update_time = (self.handler.get_cum_time(group_nodes_path) if time_mode == "Cumulative" - else self.handler.get_nodes_update_time(group_nodes_path)) - else: - update_time = cycle([None]) - for node, error, update in zip(self.nodes, nodes_errors, update_time): - if hasattr(node, 'update_ui'): - node.update_ui(error, update) - def get_update_path(self) -> List['SvGroupTreeNode']: """ Should be called only when the tree is opened in one of tree editors @@ -289,7 +275,7 @@ def absolute_location(self): return recursive_framed_location_finder(self, self.location[:]) -class SvGroupTreeNode(BaseNode, bpy.types.NodeCustomGroup): +class SvGroupTreeNode(SverchCustomTreeNode, bpy.types.NodeCustomGroup): """Node for keeping sub trees""" bl_idname = 'SvGroupTreeNode' bl_label = 'Group node (Alpha)' @@ -307,6 +293,7 @@ def nested_tree_filter(self, context): def update_group_tree(self, context): """Apply filtered tree to `node_tree` attribute. By this attribute Blender is aware of linking between the node and nested tree.""" + TreeHandler.send(ev.TreesGraphEvent()) self.node_tree: SvGroupTree = self.group_tree # also default values should be fixed if self.node_tree: @@ -390,6 +377,10 @@ def process(self): return self.node_tree: SvGroupTree + + # most simple way to pass data about whether node group should show timings + self.node_tree.sv_show_time_nodes = self.id_data.sv_show_time_nodes + input_node = self.active_input() output_node = self.active_output() if not input_node or not output_node: @@ -400,10 +391,9 @@ def process(self): break out_s.sv_set(in_s.sv_get(deepcopy=False)) - tree = UpdateTree.get(self.node_tree) - if tree.outdated_nodes is not None: - tree.outdated_nodes.add(input_node) - tree.update() + tree = gus.GroupUpdateTree.get(self.node_tree, refresh_tree=True) + tree.add_outdated([input_node]) + tree.update(self) for node in self.node_tree.nodes: if err := node.get(ERROR_KEY): @@ -414,32 +404,6 @@ def process(self): break out_s.sv_set(in_s.sv_get(deepcopy=False)) - def updater(self, group_nodes_path: Optional[List['SvGroupTreeNode']] = None, - is_input_changed: bool = True, - trees_ui_to_update: set = None) -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: - """ - This method should be called by group tree handler - is_input_changed should be False if update is called just for inspection of inner changes - """ - # todo what if the node is disabled? - # there is nothing to update, return empty iterator - if self.node_tree is None: - return empty_updater(is_output_changed=False, error=None) - - if group_nodes_path is None: - group_nodes_path = [] - else: - # copy the path otherwise if base tree has several group nodes second and next nodes will get wrong path - group_nodes_path = group_nodes_path.copy() - group_nodes_path.append(self) - - self.node_tree: SvGroupTree - input_node = self.active_input() if is_input_changed else None - return self.node_tree.handler.update(GroupEvent(GroupEvent.GROUP_NODE_UPDATE, - group_nodes_path, - [input_node] if input_node else []), - trees_ui_to_update or set()) - def active_input(self) -> Optional[bpy.types.Node]: # https://developer.blender.org/T82350 for node in reversed(self.node_tree.nodes): @@ -451,10 +415,7 @@ def active_output(self) -> Optional[bpy.types.Node]: if node.bl_idname == 'NodeGroupOutput': return node - def update(self): - if 'init_tree' in self.id_data: # tree is building by a script - let it do this - return - + def sv_update(self): # this code should work only first time a socket was added if self.node_tree: for n_in_s, t_in_s in zip(self.inputs, self.node_tree.inputs): @@ -464,10 +425,11 @@ def update(self): if hasattr(t_in_s, 'default_type'): n_in_s.default_property_type = t_in_s.default_type - update_ui = UpdateNodes.update_ui # don't want to inherit from the class (at least now) + def sv_copy(self, original): + TreeHandler.send(ev.TreesGraphEvent()) - def free(self): - self.update_ui() + def sv_free(self): + TreeHandler.send(ev.TreesGraphEvent()) class PlacingNodeOperator: @@ -848,9 +810,8 @@ def execute(self, context): sub_tree: SvGroupTree = context.node.node_tree context.space_data.path.append(sub_tree, node=group_node) sub_tree.group_node_name = group_node.name - group_nodes_path = sub_tree.get_update_path() - sub_tree.handler.send(GroupEvent(GroupEvent.EDIT_GROUP_NODE, group_nodes_path, - (n for n in sub_tree.nodes if BlNode(n).is_debug_node))) + event = ev.GroupTreeEvent(sub_tree, sub_tree.get_update_path()) + sub_tree.handler.send(event) # todo make protection from editing the same trees in more then one area # todo add the same logic to exit from tree operator return {'FINISHED'} @@ -941,12 +902,6 @@ def process(self): class NodeReroute(BaseNode): """Add sv logic""" # `copy` attribute can't be overridden for this class - # current sv_get method of sockets jump from reroute to reroute until get to normal node to get data - # def process(self): - # try: - # self.outputs[0].sv_set(self.inputs[0].sv_get(deepcopy=False)) - # except AttributeError: - # pass # in main tree reroutes still have color sockets @extend_blender_class diff --git a/core/socket_data.py b/core/socket_data.py index ce5aef1c13..a169f385f4 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -100,7 +100,7 @@ def _update_sockets(self): for tree in BlTrees().sv_trees: for node in tree.nodes: for sock in chain(node.inputs, node.outputs): - if sock.bl_idname == 'NodeSocketVirtual': + if sock.bl_idname in {'NodeSocketVirtual', 'NodeSocketColor'}: continue if sock.socket_id in self._id_sock: ds = self._id_sock[sock.socket_id] diff --git a/core/tasks.py b/core/tasks.py index f6e816b9a5..0f9555e501 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -41,6 +41,7 @@ def run(self): while self.current: if duration > max_duration: return + # print(f"Run task: {self.current}") duration += self.current.run(max_duration-duration) if self.current.last_node: msg = f'Pres "ESC" to abort, updating node "{self.current.last_node.name}"' @@ -166,6 +167,9 @@ def __eq__(self, other: 'Task'): def __hash__(self): return hash(self.tree.tree_id) + def __repr__(self): + return f"" + @post_load_call def post_load_register(): diff --git a/core/update_system.py b/core/update_system.py index 5951c303c0..f11f1ca823 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -6,16 +6,16 @@ from typing import TYPE_CHECKING, Optional, Generator, Iterable from bpy.types import Node, NodeSocket, NodeTree, NodeLink -from sverchok.core.events import TreeEvent, AnimationEvent, SceneEvent, PropertyEvent, ForceEvent, FileEvent +import sverchok.core.events as ev import sverchok.core.tasks as ts from sverchok.core.socket_conversions import ConversionPolicies from sverchok.utils.profile import profile from sverchok.utils.logging import log_error from sverchok.utils.tree_walk import bfs_walk -from sverchok.utils.handle_blender_data import BlTrees if TYPE_CHECKING: - from sverchok.node_tree import SverchCustomTreeNode as SvNode + from sverchok.node_tree import (SverchCustomTreeNode as SvNode, + SverchCustomTree as SvTree) UPDATE_KEY = "US_is_updated" @@ -23,53 +23,57 @@ TIME_KEY = "US_time" -def control_center(event: TreeEvent): +def control_center(event): """ 1. Update tree model lazily 2. Check whether the event should be processed 3. Process event or create task to process via timer""" + was_executed = False # frame update # This event can't be handled via NodesUpdater during animation rendering # because new frame change event can arrive before timer finishes its tusk. # Or timer can start working before frame change is handled. - if isinstance(event, AnimationEvent): + if type(event) is ev.AnimationEvent: + was_executed = True if event.tree.sv_animate: - Tree.get(event.tree).is_animation_updated = False - Tree.update_animation(event) + UpdateTree.get(event.tree).is_animation_updated = False + UpdateTree.update_animation(event) # something changed in the scene - elif isinstance(event, SceneEvent): + elif type(event) is ev.SceneEvent: + was_executed = True if event.tree.sv_scene_update and event.tree.sv_process: - Tree.get(event.tree).is_scene_updated = False - ts.tasks.add(ts.Task(event.tree, Tree.main_update(event.tree))) + UpdateTree.get(event.tree).is_scene_updated = False + ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) # nodes changed properties - elif isinstance(event, PropertyEvent): - tree = Tree.get(event.tree) - if tree.outdated_nodes is not None: - tree.outdated_nodes.update(event.updated_nodes) + elif type(event) is ev.PropertyEvent: + was_executed = True + tree = UpdateTree.get(event.tree) + tree.add_outdated(event.updated_nodes) if event.tree.sv_process: - ts.tasks.add(ts.Task(event.tree, Tree.main_update(event.tree))) + ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) # update the whole tree anyway - elif isinstance(event, ForceEvent): - Tree.reset_tree(event.tree) - ts.tasks.add(ts.Task(event.tree, Tree.main_update(event.tree))) + elif type(event) is ev.ForceEvent: + was_executed = True + UpdateTree.reset_tree(event.tree) + ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) # mark that the tree topology has changed - elif isinstance(event, TreeEvent): - Tree.get(event.tree).is_updated = False + elif type(event) is ev.TreeEvent: + was_executed = True + UpdateTree.get(event.tree).is_updated = False if event.tree.sv_process: - ts.tasks.add(ts.Task(event.tree, Tree.main_update(event.tree))) + ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) # new file opened - elif isinstance(event, FileEvent): - Tree.reset_tree() + elif type(event) is ev.FileEvent: + was_executed = True + UpdateTree.reset_tree() - # Unknown event - else: - raise TypeError(f'Detected unknown {event=}') + return was_executed class SearchTree: @@ -164,13 +168,13 @@ def _remove_reroutes(self): # todo add links between wifi nodes -class Tree(SearchTree): +class UpdateTree(SearchTree): """It catches some data for more efficient searches compare to Blender tree data structure""" - _tree_catch: dict[str, 'Tree'] = dict() # the module should be auto-reloaded to prevent crashes + _tree_catch: dict[str, 'UpdateTree'] = dict() # the module should be auto-reloaded to prevent crashes @classmethod - def get(cls, tree: NodeTree, refresh_tree=False) -> 'Tree': + def get(cls, tree: "SvTree", refresh_tree=False) -> "UpdateTree": """ :refresh_tree: if True it will convert update flags into outdated nodes. This can be expensive so it should be called only before tree @@ -188,13 +192,13 @@ def get(cls, tree: NodeTree, refresh_tree=False) -> 'Tree': _tree = old.copy() # update outdated nodes list - if _tree.outdated_nodes is not None: + if _tree._outdated_nodes is not None: if not _tree.is_updated: - _tree.outdated_nodes.update(_tree._update_difference(old)) + _tree._outdated_nodes.update(_tree._update_difference(old)) if not _tree.is_animation_updated: - _tree.outdated_nodes.update(_tree._animation_nodes()) + _tree._outdated_nodes.update(_tree._animation_nodes()) if not _tree.is_scene_updated: - _tree.outdated_nodes.update(_tree._scene_nodes()) + _tree._outdated_nodes.update(_tree._scene_nodes()) _tree.is_updated = True _tree.is_animation_updated = True @@ -204,7 +208,7 @@ def get(cls, tree: NodeTree, refresh_tree=False) -> 'Tree': @classmethod @profile(section="UPDATE") - def update_animation(cls, event: AnimationEvent): + def update_animation(cls, event: ev.AnimationEvent): try: g = cls.main_update(event.tree, event.is_frame_changed, not event.is_animation_playing) while True: @@ -239,15 +243,7 @@ def reset_tree(cls, tree: NodeTree = None): else: cls._tree_catch.clear() - def update(self): - walker = self._walk() - # walker = tree._debug_color(walker) - for node, prev_socks in walker: - with AddStatistic(node): - prepare_input_data(prev_socks, node.inputs) - node.process() - - def copy(self) -> 'Tree': + def copy(self) -> 'UpdateTree': """They copy will be with new topology if original tree was changed since berth of the first tree. Other attributes copied as is.""" copy_ = type(self)(self._tree) @@ -255,6 +251,10 @@ def copy(self) -> 'Tree': setattr(copy_, attr, copy(getattr(self, attr))) return copy_ + def add_outdated(self, nodes: Iterable): + if self._outdated_nodes is not None: + self._outdated_nodes.update(nodes) + def __init__(self, tree: NodeTree): super().__init__(tree) self._tree_catch[tree.tree_id] = self @@ -262,7 +262,7 @@ def __init__(self, tree: NodeTree): self.is_updated = True # False if topology was changed self.is_animation_updated = True self.is_scene_updated = True - self.outdated_nodes: Optional[set[SvNode]] = None # None means outdated all + self._outdated_nodes: Optional[set[SvNode]] = None # None means outdated all # https://stackoverflow.com/a/68550238 self._sort_nodes = lru_cache(maxsize=1)(self._sort_nodes) @@ -271,7 +271,7 @@ def __init__(self, tree: NodeTree): 'is_updated', 'is_animation_updated', 'is_scene_updated', - 'outdated_nodes', + '_outdated_nodes', ] def _animation_nodes(self) -> set['SvNode']: @@ -294,50 +294,30 @@ def _scene_nodes(self) -> set['SvNode']: def _walk(self) -> tuple[Node, list[NodeSocket]]: # walk all nodes in the tree - if self.outdated_nodes is None: + if self._outdated_nodes is None: outdated = None - self.outdated_nodes = set() + self._outdated_nodes = set() # walk triggered nodes and error nodes from previous updates else: - outdated = frozenset(self.outdated_nodes) - self.outdated_nodes.clear() # todo what if execution was canceled? + outdated = frozenset(self._outdated_nodes) + self._outdated_nodes.clear() # todo what if execution was canceled? for node, other_socks in self._sort_nodes(outdated): # execute node only if all previous nodes are updated if all(n.get(UPDATE_KEY, True) for sock in other_socks if (n := self._sock_node.get(sock))): yield node, other_socks if node.get(ERROR_KEY, False): - self.outdated_nodes.add(node) + self._outdated_nodes.add(node) else: node[UPDATE_KEY] = False - def _sort_nodes(self, outdated_nodes: frozenset['SvNode'] = None) -> list[tuple['SvNode', list[NodeSocket]]]: - # print(f"Sort nodes {self._tree.name}") - def node_walker(node_: 'SvNode'): - for nn in self._to_nodes.get(node_, []): - yield nn - - nodes = [] - if outdated_nodes is None: - for node in TopologicalSorter(self._from_nodes).static_order(): - nodes.append((node, [self._from_sock.get(s) for s in node.inputs])) - else: - outdated_nodes = set(bfs_walk(outdated_nodes, node_walker)) - from_outdated: dict[SvNode, set[SvNode]] = defaultdict(set) - for n in outdated_nodes: - if n in self._from_nodes: - from_outdated[n] = {_n for _n in self._from_nodes[n] if _n in outdated_nodes} - for node in TopologicalSorter(from_outdated).static_order(): - nodes.append((node, [self._from_sock.get(s) for s in node.inputs])) - return nodes - - def __sort_nodes(self, + def _sort_nodes(self, from_nodes: frozenset['SvNode'] = None, to_nodes: frozenset['SvNode'] = None)\ -> list[tuple['SvNode', list[NodeSocket]]]: nodes_to_walk = set() walk_structure = None - if not from_nodes and not to_nodes: + if from_nodes is None and to_nodes is None: walk_structure = self._from_nodes elif from_nodes and to_nodes: from_ = self.nodes_from(from_nodes) @@ -361,7 +341,7 @@ def __sort_nodes(self, nodes.append((node, [self._from_sock.get(s) for s in node.inputs])) return nodes - def _update_difference(self, old: 'Tree') -> set['SvNode']: + def _update_difference(self, old: 'UpdateTree') -> set['SvNode']: nodes_to_update = self._from_nodes.keys() - old._from_nodes.keys() new_links = self._links - old._links for from_sock, to_sock in new_links: @@ -444,6 +424,7 @@ def prepare_input_data(prev_socks, input_socks): def update_ui(tree: NodeTree): + # todo cumulative time errors = (n.get(ERROR_KEY, None) for n in tree.nodes) times = (n.get(TIME_KEY, 0) for n in tree.nodes) tree.update_ui(errors, times) diff --git a/node_tree.py b/node_tree.py index 075e2dcc57..006963cba3 100644 --- a/node_tree.py +++ b/node_tree.py @@ -14,7 +14,7 @@ from bpy.types import NodeTree from sverchok.core.sv_custom_exceptions import SvNoDataError -from sverchok.core.events import TreeEvent, ForceEvent, PropertyEvent, SceneEvent, AnimationEvent +import sverchok.core.events as ev from sverchok.core.main_tree_handler import TreeHandler from sverchok.data_structure import classproperty, post_load_call from sverchok.utils import get_node_class_reference @@ -31,6 +31,11 @@ class SvNodeTreeCommon: """Common class for all Sverchok trees (regular trees and group ones)""" tree_id_memory: StringProperty(default="") # identifier of the tree, should be used via `tree_id` property + sv_show_time_nodes: BoolProperty( + name="Node times", + default=False, + options=set(), + update=lambda s, c: TreeHandler.send(ev.TreeEvent(s))) @property def tree_id(self): @@ -69,6 +74,15 @@ def init_tree(self): finally: del self['init_tree'] + def update_ui(self, nodes_errors, update_time): + """ The method get information about node statistic of last update from the handler to show in view space + The method is usually called by main handler to reevaluate view of the nodes in the tree + even if the tree is not in the Live update mode""" + update_time = update_time if self.sv_show_time_nodes else cycle([None]) + for node, error, update in zip(self.nodes, nodes_errors, update_time): + if hasattr(node, 'update_ui'): + node.update_ui(error, update) + class SverchCustomTree(NodeTree, SvNodeTreeCommon): ''' Sverchok - architectural node programming of geometry in low level ''' @@ -111,13 +125,11 @@ def on_draft_mode_changed(self, context): name="Process", default=True, description='Update upon tree and node property changes', - update=lambda s, c: TreeHandler.send(TreeEvent(s)), + update=lambda s, c: TreeHandler.send(ev.TreeEvent(s)), options=set(), ) sv_animate: BoolProperty(name="Animate", default=True, description='Animate this layout', options=set()) sv_show: BoolProperty(name="Show", default=True, description='Show this layout', update=turn_off_ng, options=set()) - sv_show_time_graph: BoolProperty(name="Time Graph", default=False, options=set()) # todo is not used now - sv_show_time_nodes: BoolProperty(name="Node times", default=False, options=set(), update=lambda s, c: s.update_ui()) show_time_mode: EnumProperty( items=[(n, n, '') for n in ["Per node", "Cumulative"]], options=set(), @@ -144,38 +156,29 @@ def on_draft_mode_changed(self, context): def update(self): """This method is called if collection of nodes or links of the tree was changed""" - TreeHandler.send(TreeEvent(self)) + TreeHandler.send(ev.TreeEvent(self)) def force_update(self): """Update whole tree from scratch""" # ideally we would never like to use this method but we live in the real world - TreeHandler.send(ForceEvent(self)) + TreeHandler.send(ev.ForceEvent(self)) def update_nodes(self, nodes, cancel=True): """This method expects to get list of its nodes which should be updated""" - return TreeHandler.send(PropertyEvent(self, nodes)) + return TreeHandler.send(ev.PropertyEvent(self, nodes)) def scene_update(self): """This method should be called by scene changes handler it ignores events related with S sverchok trees in other cases it updates nodes which read data from Blender""" - TreeHandler.send(SceneEvent(self)) + TreeHandler.send(ev.SceneEvent(self)) def process_ani(self, frame_changed: bool, animation_playing: bool): """ Process the Sverchok node tree if animation layers show true. For animation callback/handler """ - TreeHandler.send(AnimationEvent(self, frame_changed, animation_playing)) - - def update_ui(self, nodes_errors, update_time): - """ The method get information about node statistic of last update from the handler to show in view space - The method is usually called by main handler to reevaluate view of the nodes in the tree - even if the tree is not in the Live update mode""" - update_time = update_time if self.sv_show_time_nodes else cycle([None]) - for node, error, update in zip(self.nodes, nodes_errors, update_time): - if hasattr(node, 'update_ui'): - node.update_ui(error, update) + TreeHandler.send(ev.AnimationEvent(self, frame_changed, animation_playing)) class UpdateNodes: diff --git a/nodes/logic/evolver.py b/nodes/logic/evolver.py index 3a7d26f7c3..e776f77073 100644 --- a/nodes/logic/evolver.py +++ b/nodes/logic/evolver.py @@ -18,7 +18,7 @@ from bpy.props import ( BoolProperty, StringProperty, EnumProperty, IntProperty, FloatProperty) -from sverchok.core.update_system import Tree +from sverchok.core.update_system import UpdateTree from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode from sverchok.utils.sv_operator_mixins import SvGenericNodeLocator @@ -356,7 +356,7 @@ def fill_genes(self, random_val=True): agent_gene = gene.init_val self.genes.append(agent_gene) - def evaluate_fitness(self, tree, node, s_tree: Tree, exec_order): + def evaluate_fitness(self, tree, node, s_tree: UpdateTree, exec_order): try: tree.sv_process = False for gen_data, agent_gene in zip(self.genes_def, self.genes): @@ -410,7 +410,7 @@ def __init__(self, genotype_frame, node, tree): self.population_g: list[DNA] = [] self.init_population(node.population_n) - self._tree = Tree.get(tree) + self._tree = UpdateTree.get(tree) exec_order = self._tree.nodes_from([tree.nodes[g.name] for g in self.genes]) self.exec_order = self._tree.sort_nodes(exec_order) diff --git a/nodes/logic/loop_out.py b/nodes/logic/loop_out.py index 217b66af5c..bef8344528 100644 --- a/nodes/logic/loop_out.py +++ b/nodes/logic/loop_out.py @@ -18,7 +18,7 @@ import bpy from bpy.props import EnumProperty -from sverchok.core.update_system import Tree +from sverchok.core.update_system import UpdateTree from sverchok.node_tree import SverchCustomTreeNode @@ -214,7 +214,7 @@ def for_each_mode(self, loop_in_node): for outp in self.outputs: outp.sv_set([]) else: - tree = Tree.get(self.id_data) + tree = UpdateTree.get(self.id_data) from_nodes = tree.nodes_from([loop_in_node]) to_nodes = tree.nodes_to([self]) loop_nodes = from_nodes.intersection(to_nodes) @@ -281,7 +281,7 @@ def range_mode(self, loop_in_node): for inp, outp in zip(self.inputs[2:], self.outputs): outp.sv_set(inp.sv_get(deepcopy=False, default=[])) else: - tree = Tree.get(self.id_data) + tree = UpdateTree.get(self.id_data) from_nodes = tree.nodes_from([loop_in_node]) to_nodes = tree.nodes_to([self]) loop_nodes = from_nodes.intersection(to_nodes) diff --git a/utils/geom.py b/utils/geom.py index ae6ba5fa3c..fd2a27e14d 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -1146,7 +1146,7 @@ def intersect_with_plane(self, plane2): if plane2.is_parallel(line1) or plane2.is_parallel(line2): v1_new = v1 + v2 v2_new = v1 - v2 - debug("{}, {} => {}, {}".format(v1, v2, v1_new, v2_new)) + # debug("{}, {} => {}, {}".format(v1, v2, v1_new, v2_new)) line1 = LineEquation.from_direction_and_point(v1_new, p0) line2 = LineEquation.from_direction_and_point(v2_new, p0) diff --git a/utils/tree_walk.py b/utils/tree_walk.py index d34639e867..cbcc2485d9 100644 --- a/utils/tree_walk.py +++ b/utils/tree_walk.py @@ -11,12 +11,13 @@ from abc import ABC, abstractmethod from collections import deque from itertools import count -from typing import Generator, List, TypeVar, Generic, Callable, Iterable +from typing import Generator, List, TypeVar, Generic, Callable, Iterable, Iterator T = TypeVar('T') +NextFunc = Callable[[T], Iterable[T]] -def bfs_walk(nodes: Iterable[T], next_: Callable[[T], Iterable[T]]) -> Generator[T, None, None]: +def bfs_walk(nodes: Iterable[T], next_: NextFunc) -> Iterator[T]: """ Walk from the current node, it will visit all next nodes First will be visited children nodes than children of children nodes etc. @@ -39,6 +40,17 @@ def bfs_walk(nodes: Iterable[T], next_: Callable[[T], Iterable[T]]) -> Generator f'or most likely it is circular') +def recursion_dfs_walk(nodes: Iterable[T], next_: NextFunc, _count=None) -> Iterator[T]: + if _count is None: + _count = count(1) + for node in nodes: + yield node + yield from recursion_dfs_walk(next_(node), next_, _count) + if (i := next(_count)) and i > 20000: + raise RecursionError(f'The tree has either more then={20000} nodes ' + f'or most likely it is circular') + + class Node(ABC): @property @abstractmethod From db9529bdf62cd9874d8124dab604759003b10bc4 Mon Sep 17 00:00:00 2001 From: Durman Date: Mon, 30 May 2022 09:35:30 +0400 Subject: [PATCH 17/25] add support of cancelling execution of new update system --- core/group_update_system.py | 2 +- core/main_tree_handler.py | 328 +----------------------------------- core/tasks.py | 1 + core/update_system.py | 16 +- ui/nodeview_keymaps.py | 6 +- 5 files changed, 21 insertions(+), 332 deletions(-) diff --git a/core/group_update_system.py b/core/group_update_system.py index 97d6afa2ba..ad0b4a9c4e 100644 --- a/core/group_update_system.py +++ b/core/group_update_system.py @@ -63,7 +63,7 @@ def update(self, node: 'GrNode'): self._viewer_nodes = {node.active_output()} walker = self._walk() - walker = self._debug_color(walker) + # walker = self._debug_color(walker) for node, prev_socks in walker: with us.AddStatistic(node): us.prepare_input_data(prev_socks, node.inputs) diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py index 8338b9d4d6..829af97374 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -7,19 +7,11 @@ from __future__ import annotations -import gc -from contextlib import contextmanager -from time import time -from typing import Dict, Generator, Optional, Iterator, Tuple, NewType, List, TYPE_CHECKING, Callable - -import bpy -from sverchok.core.sv_custom_exceptions import SvNoDataError, CancelError -from sverchok.core.socket_conversions import ConversionPolicies +from typing import Dict, NewType, List, TYPE_CHECKING + import sverchok.core.events as ev -from sverchok.utils.logging import debug, log_error -from sverchok.utils.tree_structure import Tree, Node -from sverchok.utils.handle_blender_data import BlTrees, BlNode -from sverchok.utils.profile import profile +from sverchok.utils.tree_structure import Tree +from sverchok.utils.handle_blender_data import BlNode import sverchok.core.update_system as us import sverchok.core.group_update_system as gus @@ -39,7 +31,7 @@ def send(event): """Main control center 1. preprocess the event 2. Pass the event to update system(s)""" - # print(f"{event=}") + print(f"{event=}") # something changed in scene and it duplicates some tree events which should be ignored if isinstance(event, ev.SceneEvent): @@ -60,229 +52,6 @@ def send(event): elif results == 0: raise RuntimeError(f"{event} was not handled") - @staticmethod - def get_error_nodes(bl_tree) -> Iterator[Optional[Exception]]: - """Return map of bool values to group tree nodes where node has error if value is True""" - tree = ContextTrees.get(bl_tree, rebuild=False) - for node in bl_tree.nodes: - if node.bl_idname in {'NodeReroute', 'NodeFrame'}: - yield None - continue - with tree.set_exec_context(): # tests shows good performance frequent use of the context manager - error = tree.nodes[node.name].error - # exit context manager before yielding otherwise it will block reading context dependent properties - yield error - - @staticmethod - def get_update_time(bl_tree) -> Iterator[Optional[float]]: - tree = ContextTrees.get(bl_tree, rebuild=False) - for node in bl_tree.nodes: - if node.bl_idname in {'NodeReroute', 'NodeFrame'}: - yield None - continue - with tree.set_exec_context(): - upd_time = tree.nodes[node.name].update_time - yield upd_time - - @staticmethod - def get_cum_time(bl_tree) -> Iterator[Optional[float]]: - cum_time_nodes = ContextTrees.calc_cam_update_time(bl_tree) - for node in bl_tree.nodes: - yield cum_time_nodes.get(node) - - -class Task: - _task: Optional['Task'] = None # for now running only one task is supported - - __slots__ = ('event', - '_handler_func', - '_handler', - '_node_tree_area', - '_start_time', - '_last_node', - ) - - @classmethod - def add(cls, event: ev.TreeEvent, handler: Callable) -> 'Task': - if cls._task and cls._task.is_running(): - raise RuntimeError(f"Can't update tree: {event.tree.name}," - f" already updating tree: {cls._task.event.tree.name}") - cls._task = cls(event, handler) - return cls._task - - @classmethod - def get(cls) -> Optional['Task']: - return cls._task - - def __init__(self, event, handler): - self.event: ev.TreeEvent = event - self._handler_func: Callable[[ev.TreeEvent], Generator] = handler - self._handler: Optional[Generator[SvNode, None, None]] = None - self._node_tree_area: Optional[bpy.types.Area] = None - self._start_time: Optional[float] = None - self._last_node: Optional[SvNode] = None - - def start(self): - changed_tree = self.event.tree - if self.is_running(): - raise RuntimeError(f'Tree "{changed_tree.name}" already is being updated') - self._handler = self._handler_func(self.event) - - # searching appropriate area index for reporting update progress - for area in bpy.context.screen.areas: - if area.ui_type == 'SverchCustomTreeType': - path = area.spaces[0].path - if path and path[-1].node_tree.name == changed_tree.name: - self._node_tree_area = area - break - gc.disable() - - self._start_time = time() - - @profile(section="UPDATE") - def run(self): - try: - if self._last_node: - self._last_node.set_temp_color() - - start_time = time() - while (time() - start_time) < 0.15: # 0.15 is max timer frequency - node = next(self._handler) - - self._last_node = node - node.set_temp_color((0.7, 1.000000, 0.7)) - self._report_progress(f'Pres "ESC" to abort, updating node "{node.name}"') - - except StopIteration: - self.finish_task() - - def cancel(self): - try: - self._handler.throw(CancelError) - except (StopIteration, RuntimeError): - pass - finally: # protection from the task to be stack forever - self.finish_task() - - def finish_task(self): - try: - # this only need to trigger scene changes handler again - if self.event.tree.nodes: - status = self.event.tree.nodes[-1].use_custom_color - self.event.tree.nodes[-1].use_custom_color = not status - self.event.tree.nodes[-1].use_custom_color = status - - # this indicates that process of the tree is finished and next scene event can be skipped - # the scene trigger will try to update all trees, so they all should be marked - for t in BlTrees().sv_main_trees: - t['SKIP_UPDATE'] = True - - gc.enable() - debug(f'Global update - {int((time() - self._start_time) * 1000)}ms') - self._report_progress() - finally: - Task._task = None - - def is_running(self) -> bool: - return self._handler is not None - - def _report_progress(self, text: str = None): - if self._node_tree_area: - self._node_tree_area.header_text_set(text) - - -def global_updater(event_type: str) -> Generator[Node, None, None]: - """Find all Sverchok main trees and run their handlers and update their UI if necessary - update_ui of group trees will be called only if they opened in one of tree editors - update_ui of main trees will be called if they are opened or was changed during the update event""" - - # grab trees from active node group editors - trees_ui_to_update = set() - if bpy.context.screen: # during animation rendering can be None - for area in bpy.context.screen.areas: - if area.ui_type == BlTrees.MAIN_TREE_ID: - if area.spaces[0].path: # filter editors without active tree - trees_ui_to_update.add(area.spaces[0].path[-1].node_tree) - - for bl_tree in BlTrees().sv_main_trees: - was_changed = False - # update only trees which should be animated (for performance improvement in case of many trees) - if event_type == ev.TreeEvent.FRAME_CHANGE: - if bl_tree.sv_animate: - was_changed = yield from tree_updater(bl_tree, trees_ui_to_update) - - # tree should be updated any way - elif event_type == ev.TreeEvent.FORCE_UPDATE and 'FORCE_UPDATE' in bl_tree: - del bl_tree['FORCE_UPDATE'] - was_changed = yield from tree_updater(bl_tree, trees_ui_to_update) - - # this seems the event upon some changes in the tree, skip tree if the property is switched off - else: - if bl_tree.sv_process: - was_changed = yield from tree_updater(bl_tree, trees_ui_to_update) - - # it has sense to call this here if you press update all button or creating group tree from selected - if was_changed: - # if "DEBUG": - # yield None - update_ui(bl_tree) # this only will update UI of main trees - trees_ui_to_update.discard(bl_tree) # protection from double updating - - # this only need to trigger scene changes handler again - bl_tree.nodes[-1].use_custom_color = not bl_tree.nodes[-1].use_custom_color - bl_tree.nodes[-1].use_custom_color = not bl_tree.nodes[-1].use_custom_color - # this indicates that process of the tree is finished and next scene event can be skipped - bl_tree['SKIP_UPDATE'] = True - - # this will update all opened trees (in group editors) - # regardless whether the trees was changed or not, including group nodes - for bl_tree in trees_ui_to_update: - update_ui(bl_tree) - # args = [bl_tree.get_update_path()] if BlTree(bl_tree).is_group_tree else [] - # bl_tree.update_ui(*args) - - -def update_ui(tree): - nodes_errors = TreeHandler.get_error_nodes(tree) - update_time = (TreeHandler.get_cum_time(tree) if tree.show_time_mode == "Cumulative" - else TreeHandler.get_update_time(tree)) - tree.update_ui(nodes_errors, update_time) - - -def tree_updater(bl_tree, trees_ui_to_update: set) -> Generator[Node, None, bool]: - tree = ContextTrees.get(bl_tree) - tree_output_changed = False - - with tree.set_exec_context(): - for node in tree.sorted_walk(tree.output_nodes): - can_be_updated = all(n.is_updated for n in node.last_nodes) - if not can_be_updated: - # here different logic can be implemented but for this we have to know if is there any output of the node - # we could leave the node as updated and don't broke work of the rest forward nodes - # but if the node does not have any output all next nodes will gen NoDataError what is horrible - node.is_updated = False - node.is_output_changed = False - continue - - if hasattr(node.bl_tween, 'updater'): - updater = group_node_updater(node, trees_ui_to_update) - elif hasattr(node.bl_tween, 'process'): - updater = node_updater(node) - else: - updater = empty_updater(node, error=None) - - # update node with sub update system, catch statistic - start_time = time() - node_error = yield from updater - update_time = (time() - start_time) - - if node.is_output_changed or node_error: - node.error = node_error - node.update_time = None if node_error else update_time - tree_output_changed = True - - return tree_output_changed - class ContextTrees: """It keeps trees with their states""" @@ -432,90 +201,3 @@ def generate_path(group_nodes: List[SvNode]) -> Path: """path is ordered collection group node ids max length of path should be no more then number of base trees of most nested group node + 1""" return Path('.'.join(n.node_id for n in group_nodes)) - - -def node_updater(node: Node) -> Generator[Node, None, Optional[Exception]]: - """The node should has process method, all previous nodes should be updated""" - node_error = None - - previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) - should_be_updated = not node.is_updated or node.is_input_changed or previous_nodes_are_changed - - node.is_output_changed = False # it should always False unless the process method was called - node.is_input_changed = False # if node wont be able to handle new input it will be seen in its update status - if should_be_updated: - try: - yield node - with handle_node_data(node): - node.bl_tween.process() - node.is_updated = True - node.is_output_changed = True - except CancelError as e: - node.is_updated = False - node_error = e - except Exception as e: - node.is_updated = False - log_error(e) - node_error = e - return node_error - - -def group_node_updater(node: Node, trees_ui_to_update: set) -> Generator[Node, None, Tuple[bool, Optional[Exception]]]: - """The node should have updater attribute""" - previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) - should_be_updated = (not node.is_updated or node.is_input_changed or previous_nodes_are_changed) - updater = node.bl_tween.updater(is_input_changed=should_be_updated, trees_ui_to_update=trees_ui_to_update) - with handle_node_data(node): # it's is redundant if the node group has no changes - is_output_changed, out_error = yield from updater - if is_output_changed or out_error: - yield node # yield groups node so it be colored by node Updater if necessary - node.is_input_changed = False - node.is_updated = not out_error - node.is_output_changed = is_output_changed - return out_error - - -def empty_updater(node: Node = None, **kwargs): # todo to remove there is no reroute nodes in trees anymore - """Reroutes, frame nodes, empty updaters which do nothing, set node in correct state - returns given kwargs (only their values) like error=None, is_updated=True""" - if node: # ideally we would like always get first argument as node but group updater does not posses it - previous_nodes_are_changed = any(n.is_output_changed for n in node.last_nodes) - should_be_updated = not node.is_updated or node.is_input_changed or previous_nodes_are_changed - node.is_input_changed = False # if node wont be able to handle new input it will be seen in its update status - node.is_updated = True - node.is_output_changed = True if should_be_updated else False - return tuple(kwargs.values()) if len(kwargs) > 1 else next(iter(kwargs.values())) - yield - - -@contextmanager -def handle_node_data(node: Node): - """Any node should be executed inside this context manager. It supply node with data and save output node data - Also it makes data conversion if it is needed""" - - # before execution the data should be put into input sockets - # the storage of the data is in output sockets and is dependent on context - # context should be set before the function execution - for in_sock in node.inputs: - for out_sock in in_sock.linked_sockets: - data = out_sock.data - - # cast data from one socket type to another - if out_sock.bl_tween.bl_idname != in_sock.bl_tween.bl_idname: - implicit_conversions = ConversionPolicies.get_conversion(in_sock.bl_tween.default_conversion_name) - data = implicit_conversions.convert(in_sock.bl_tween, out_sock.bl_tween, data) - - # save data to input socket - in_sock.bl_tween.sv_set(data) # data should be saved without context to be able to read by node - - # pass flow for node execution - yield None - - # after node was executed the data should be reputed into appropriate place according to execution context - # this redundant step in main trees and have only sense inside node groups - for out_sock in node.outputs: - try: - if hasattr(out_sock.bl_tween, 'sv_get'): # in case the node is group input one - out_sock.data = out_sock.bl_tween.sv_get() - except SvNoDataError: - pass diff --git a/core/tasks.py b/core/tasks.py index 0f9555e501..ba8085af8f 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -83,6 +83,7 @@ def _next(self): def _finish(self): self._report_progress() + del self._main_area # this only need to trigger scene changes handler again # todo should be proved that this is right location to call from diff --git a/core/update_system.py b/core/update_system.py index f11f1ca823..a02e54d4a5 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -8,6 +8,7 @@ from bpy.types import Node, NodeSocket, NodeTree, NodeLink import sverchok.core.events as ev import sverchok.core.tasks as ts +from sverchok.core.sv_custom_exceptions import CancelError from sverchok.core.socket_conversions import ConversionPolicies from sverchok.utils.profile import profile from sverchok.utils.logging import log_error @@ -226,11 +227,14 @@ def main_update(cls, tree: NodeTree, update_nodes=True, update_interface=True) - up_tree = cls.get(tree, refresh_tree=True) walker = up_tree._walk() # walker = up_tree._debug_color(walker) - for node, prev_socks in walker: - with AddStatistic(node): - yield node - prepare_input_data(prev_socks, node.inputs) - node.process() + try: + for node, prev_socks in walker: + with AddStatistic(node): + yield node + prepare_input_data(prev_socks, node.inputs) + node.process() + except CancelError: + pass if update_interface: update_ui(tree) @@ -406,6 +410,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._node[ERROR_KEY] = repr(exc_val) if self._supress and exc_type is not None: + if issubclass(exc_type, CancelError): + return False return issubclass(exc_type, Exception) diff --git a/ui/nodeview_keymaps.py b/ui/nodeview_keymaps.py index 6e3a4d97d0..1090caf042 100644 --- a/ui/nodeview_keymaps.py +++ b/ui/nodeview_keymaps.py @@ -20,7 +20,7 @@ import bpy from sverchok.ui.development import displaying_sverchok_nodes -from sverchok.core import main_tree_handler +import sverchok.core.tasks as ts class SvToggleProcess(bpy.types.Operator): @@ -109,8 +109,8 @@ class PressingEscape(bpy.types.Operator): bl_label = 'Abort nodes updating' def execute(self, context): - if (task := main_tree_handler.Task.get()) and task.is_running(): - task.cancel() + if ts.tasks: + ts.tasks.cancel() return {'FINISHED'} @classmethod From 81f558767454275a872d1cda1ee4fe375063337f Mon Sep 17 00:00:00 2001 From: Durman Date: Mon, 30 May 2022 09:44:24 +0400 Subject: [PATCH 18/25] revert changes made in the module in the branch --- utils/tree_structure.py | 227 ++-------------------------------------- 1 file changed, 11 insertions(+), 216 deletions(-) diff --git a/utils/tree_structure.py b/utils/tree_structure.py index 5a123a9cd9..af56577b73 100644 --- a/utils/tree_structure.py +++ b/utils/tree_structure.py @@ -10,16 +10,10 @@ from collections.abc import Mapping from typing import List, Iterable, TypeVar, TYPE_CHECKING, Dict, Any, Generic, Optional, Union -from collections import defaultdict -from contextlib import contextmanager -from functools import wraps -from typing import List, Iterable, TypeVar, TYPE_CHECKING, Dict, Any, Generic, Optional, Union, NewType import bpy import sverchok.utils.tree_walk as tw -from sverchok.core.socket_data import sv_get_socket, sv_set_socket -from sverchok.utils.handle_blender_data import BlNode if TYPE_CHECKING: from sverchok.core.node_group import SvGroupTree @@ -27,17 +21,6 @@ SvNode = Union[SverchCustomTreeNode, bpy.types.Node] -def _context_dependent(func): - """Decorator for context dependent methods and properties. Raise error if context is not determined - Decorated methods should have context argument and the class should have exec_context property""" - @wraps(func) - def inner(self, *args, **kwargs): - if self.exec_context is None: - raise RuntimeError("Before execution this method/property execution context should be determined") - return func(self, *args, **kwargs) - return inner - - class Node(tw.Node): def __init__(self, name: str, index: int, tree: Tree, bl_node): self.name = name @@ -46,7 +29,10 @@ def __init__(self, name: str, index: int, tree: Tree, bl_node): self._outputs: List[Socket] = [] self._index = index self._tree = tree - self._is_input_linked = None # has links lead to group input nodes + + self.is_input_changed = False + self.is_updated = False + self.is_output_changed = False # cash self.bl_tween = bl_node @@ -56,68 +42,6 @@ def __init__(self, name: str, index: int, tree: Tree, bl_node): # """Quite expansive function, 1ms = 800 calls, it's better to cash, is potentially dangerous""" # return self._tree.bl_tween.nodes[self._index] - @property - def id(self): - return self.bl_tween.node_id - - @property - @_context_dependent - def is_updated(self): - return ContextAttributes.get(self.id, 'is_updated', False, self.exec_context) - - @is_updated.setter - @_context_dependent - def is_updated(self, status): - ContextAttributes.set(self.id, 'is_updated', status, self.exec_context) - - @is_updated.deleter - def is_updated(self): - ContextAttributes.del_attr_data(self.id, 'is_updated') - - @property - @_context_dependent - def is_input_changed(self): - return ContextAttributes.get(self.id, 'is_input_changed', True, self.exec_context) - - @is_input_changed.setter - @_context_dependent - def is_input_changed(self, status): - ContextAttributes.set(self.id, 'is_input_changed', status, self.exec_context) - - @is_input_changed.deleter - def is_input_changed(self): - ContextAttributes.del_attr_data(self.id, 'is_input_changed') - - @property - @_context_dependent - def is_output_changed(self): - return ContextAttributes.get(self.id, 'is_output_changed', True, self.exec_context) - - @is_output_changed.setter - @_context_dependent - def is_output_changed(self, status): - ContextAttributes.set(self.id, 'is_output_changed', status, self.exec_context) - - @property - @_context_dependent - def error(self) -> Exception: - return ContextAttributes.get(self.id, 'error', None, self.exec_context) - - @error.setter - @_context_dependent - def error(self, err: Exception): - ContextAttributes.set(self.id, 'error', err, self.exec_context) - - @property - @_context_dependent - def update_time(self) -> float: - return ContextAttributes.get(self.id, 'update_time', None, self.exec_context) - - @update_time.setter - @_context_dependent - def update_time(self, upd_time: float): - ContextAttributes.set(self.id, 'update_time', upd_time, self.exec_context) - @property def index(self): """Index of node location in Blender collection from which it was copied""" @@ -141,18 +65,6 @@ def last_nodes(self) -> Iterable[Node]: """Returns all nodes which are linked wia the node input sockets""" return {other_s.node for s in self.inputs for other_s in s.linked_sockets} - @property - def is_input_linked(self) -> bool: - if self._is_input_linked is None: # or should it raise an error instead? and force user to call method manually - self._tree.fill_is_input_linked() - return self._is_input_linked - - @property - def exec_context(self): - """Return tree path if node is input linked and is not a debug otherwise empty path is returned""" - return self._tree.exec_context if self.is_input_linked else '' - # return self._tree.exec_context if not BlNode(self.bl_tween).is_debug_node and self.is_input_linked else '' - def get_bl_node(self, tree: SvGroupTree) -> bpy.types.Node: """ Will return the node from given tree with the same name @@ -205,7 +117,6 @@ def __init__(self, bl_tree: SvGroupTree): self._tree_id = bl_tree.tree_id self._nodes = NodesCollection(bl_tree, self) self._links = LinksCollection(bl_tree, self) - self._exec_context = None # should be given to use tree data dependent on context (socket data, stats) # if the tree is created in the same time with the class initialization (loading file) # the index of the tree in node_groups collection will be not found (-1) @@ -230,31 +141,6 @@ def nodes(self) -> NodesCollection: def links(self) -> LinksCollection: return self._links - def fill_is_input_linked(self): # it can get type of input nodes optionally later - for node in self.nodes: - node._is_input_linked = False - for node in self.bfs_walk([self.nodes.active_input] if self.nodes.active_output else []): - node._is_input_linked = True - - @contextmanager - def set_exec_context(self, context: str = ''): # todo should be context type imported? - if self._exec_context is not None: - raise RuntimeError("Tree already has execution context") - self._exec_context = context - try: - yield None - finally: - self._exec_context = None - - @property - def exec_context(self): - return self._exec_context - - def delete(self): - """Free context data""" - for node in self.nodes: - ContextAttributes.del_obj_data(node.id) - def _handle_wifi_nodes(self): """The idea is to convert wifi nodes into regular nodes with sockets and links between them""" # todo the code is very bad and should be removed later wifi node refactoring @@ -320,13 +206,10 @@ def __sub__(self, other) -> List[Element]: class NodesCollection(TreeCollections[NodeType]): def __init__(self, bl_tree: SvGroupTree, tree: Tree): - """Generate Python representation of Blender nodes. Reroute and frame nodes are ignored""" super().__init__() self._active_input: Optional[Node] = None self._active_output: Optional[Node] = None for i, bl_node in enumerate(bl_tree.nodes): - if bl_node.bl_idname in {'NodeReroute', 'NodeFrame'}: - continue node = Node.from_bl_node(bl_node, i, tree) self._dict[bl_node.name] = node @@ -347,15 +230,7 @@ def active_output(self) -> Optional[Node]: class LinksCollection(TreeCollections): def __init__(self, bl_tree: SvGroupTree, tree: Tree): - """Generate Python representation of Blender links. Reroute nodes are thrown from the tree model. - Muted links are ignored""" super().__init__() - - # helping data structure for fast link search - from_node_links = defaultdict(list) - for bl_link in bl_tree.links: - from_node_links[bl_link.from_node].append(bl_link) - for i, bl_link in enumerate(bl_tree.links): # new in 2.93, it is the same as if there was no the link (is_hidden was added before 2.93) @@ -363,34 +238,13 @@ def __init__(self, bl_tree: SvGroupTree, tree: Tree): # or bl_link.is_hidden: # it does not call update method of a tree https://developer.blender.org/T89109 continue - # link from reroute node to be ignored - from_bl_node = bl_link.from_node - if from_bl_node.bl_idname == 'NodeReroute': - continue + from_node = tree.nodes[bl_link.from_node.name] + from_socket = from_node.get_output_socket(bl_link.from_socket.identifier) + to_node = tree.nodes[bl_link.to_node.name] + to_socket = to_node.get_input_socket(bl_link.to_socket.identifier) - # link to normal node should be found - to_bl_node = bl_link.to_node - if to_bl_node.bl_idname == 'NodeReroute': - next_links = from_node_links[bl_link.to_node].copy() - to_links = [] - while next_links: - next_link = next_links.pop() - if next_link.to_node.bl_idname == 'NodeReroute': - next_links.extend(from_node_links[next_link.to_node]) - else: - to_links.append(next_link) - else: - to_links = [bl_link] - - # generate link(s) - for to_link in to_links: - from_node = tree.nodes[bl_link.from_node.name] - from_socket = from_node.get_output_socket(bl_link.from_socket.identifier) - to_node = tree.nodes[to_link.to_node.name] - to_socket = to_node.get_input_socket(to_link.to_socket.identifier) - - self._dict[(from_node.name, from_socket.identifier, - to_node.name, to_socket.identifier)] = Link(from_socket, to_socket, i) + self._dict[(bl_link.from_node.name, bl_link.from_socket.identifier, + bl_link.to_node.name, bl_link.to_socket.identifier)] = Link(from_socket, to_socket, i) def __iter__(self) -> Iterable[Link]: return super().__iter__() @@ -419,20 +273,6 @@ def node(self) -> Node: def links(self) -> List[Link]: return self._links - @property - def exec_context(self): - return self._node.exec_context - - @property - @_context_dependent - def data(self): - return sv_get_socket(self.bl_tween, False, self.exec_context) - - @data.setter - @_context_dependent - def data(self, data): - sv_set_socket(self.bl_tween, data, self.exec_context) - def get_bl_socket(self, bl_tree: SvGroupTree) -> bpy.types.NodeSocket: """Search socket in given tree by its identifier""" bl_node = self.node.get_bl_node(bl_tree) @@ -481,49 +321,4 @@ def to_node(self) -> Node: def __repr__(self): return f'FROM "{self.from_node.name}.{self.from_socket.identifier}" ' \ - f'TO "{self.to_node.name}.{self.to_socket.identifier}"' - - -class ContextAttributes: - """It keeps attributes which should be preserved between tree evaluations - also it should keep those attributes which can have different values depending on context execution - first scenario related with nature of Blender tree data structure which each time should be converted into - Python data structure for efficient search - second scenario is related with nature of node groups where a node can have different input dependent on - a group node from which execution has began""" - - ObjectId = NewType('ObjectId', str) - AttrName = NewType('AttrName', str) - Context = NewType('Context', str) # context id - DataAddress = Dict[ObjectId, Dict[AttrName, Dict[Context, Any]]] - _socket_data_cache: DataAddress = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: None))) - - @classmethod - def set(cls, object_id: ObjectId, attr_name: AttrName, data, context: Context = ''): - """Save data of attribute of an object. Context allow ot have multiple values per attribute""" - cls._socket_data_cache[object_id][attr_name][context] = data - - @classmethod - def get(cls, object_id: ObjectId, attr_name: AttrName, default=..., context: Context = ''): - """Get saved data of attribute of given object. Context determines multiple values for the same attribute""" - data = cls._socket_data_cache[object_id][attr_name][context] - if data is None and default is ...: - raise LookupError("Given object does not have any data") - else: - return default if data is None else data - - @classmethod - def del_obj_data(cls, object_id: ObjectId): - """Deletes all attributes of given object""" - try: - del cls._socket_data_cache[object_id] - except KeyError: - pass - - @classmethod - def del_attr_data(cls, object_id: ObjectId, attr_name: AttrName): - """Delete all data of attribute of given object""" - try: - del cls._socket_data_cache[object_id][attr_name] - except KeyError: - pass + f'TO "{self.to_node.name}.{self.to_socket.identifier}"' \ No newline at end of file From 03243efef7acc860b1c96abd660d324df63c9d95 Mon Sep 17 00:00:00 2001 From: Durman Date: Mon, 30 May 2022 14:26:50 +0400 Subject: [PATCH 19/25] add cumulative time support for new update system main_tree_handler module refactoring --- core/__init__.py | 4 +- core/event_system.py | 40 +++++++ core/group_update_system.py | 6 +- core/handlers.py | 4 +- core/main_tree_handler.py | 203 ------------------------------------ core/node_group.py | 23 ++-- core/update_system.py | 37 +++++-- node_tree.py | 28 ++--- settings.py | 1 - 9 files changed, 103 insertions(+), 243 deletions(-) create mode 100644 core/event_system.py delete mode 100644 core/main_tree_handler.py diff --git a/core/__init__.py b/core/__init__.py index 0657eeb5a5..5f0e696089 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -12,12 +12,14 @@ core_modules = [ "sv_custom_exceptions", "update_system", "sockets", "socket_data", - "handlers", "main_tree_handler", + "handlers", "events", "node_group", "tasks", "group_update_system", + "event_system", ] + def sv_register_modules(modules): for m in modules: if m.__name__ != "sverchok.menu": diff --git a/core/event_system.py b/core/event_system.py new file mode 100644 index 0000000000..f279284e3e --- /dev/null +++ b/core/event_system.py @@ -0,0 +1,40 @@ +# 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 + +import sverchok.core.events as ev +import sverchok.core.update_system as us +import sverchok.core.group_update_system as gus + + +update_systems = [us.control_center, gus.control_center] + + +def handle_event(event): + """Main control center + 1. preprocess the event + 2. Pass the event to update system(s)""" + # print(f"{event=}") + + # something changed in scene + if type(event) is ev.SceneEvent: + # this event was caused by update system itself and should be ignored + if 'SKIP_UPDATE' in event.tree: + del event.tree['SKIP_UPDATE'] + return + + was_handled = dict() + for handler in update_systems: + res = handler(event) + was_handled[handler] = res + + if (results := sum(was_handled.values())) > 1: + duplicates = [f.__module__ for f, r in was_handled if r == 1] + raise RuntimeError(f"{event=} was executed more than one time, {duplicates=}") + elif results == 0: + raise RuntimeError(f"{event} was not handled") diff --git a/core/group_update_system.py b/core/group_update_system.py index ad0b4a9c4e..09767d67db 100644 --- a/core/group_update_system.py +++ b/core/group_update_system.py @@ -70,7 +70,11 @@ def update(self, node: 'GrNode'): node.process() if is_opened_tree: - us.update_ui(self._tree) + if self._tree.show_time_mode == "Cumulative": + times = self.calc_cam_update_time() + else: + times = None + us.update_ui(self._tree, times) except Exception: raise diff --git a/core/handlers.py b/core/handlers.py index ad9e5fc1c2..0ab31480d2 100644 --- a/core/handlers.py +++ b/core/handlers.py @@ -4,6 +4,7 @@ from sverchok import old_nodes from sverchok import data_structure import sverchok.core.events as ev +from sverchok.core.event_system import handle_event from sverchok.core.socket_data import clear_all_socket_cache from sverchok.ui import bgl_callback_nodeview, bgl_callback_3dview from sverchok.utils import app_handler_ops @@ -158,8 +159,7 @@ def sv_pre_load(scene): clear_all_socket_cache() sv_clean(scene) - import sverchok.core.main_tree_handler as mh - mh.TreeHandler.send(ev.FileEvent()) + handle_event(ev.FileEvent()) @persistent diff --git a/core/main_tree_handler.py b/core/main_tree_handler.py deleted file mode 100644 index 829af97374..0000000000 --- a/core/main_tree_handler.py +++ /dev/null @@ -1,203 +0,0 @@ -# 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 typing import Dict, NewType, List, TYPE_CHECKING - -import sverchok.core.events as ev -from sverchok.utils.tree_structure import Tree -from sverchok.utils.handle_blender_data import BlNode -import sverchok.core.update_system as us -import sverchok.core.group_update_system as gus - -if TYPE_CHECKING: - from sverchok.core.node_group import SvGroupTreeNode as SvNode - - -Path = NewType('Path', str) # concatenation of group node ids - -update_systems = [us.control_center, gus.control_center] - - -class TreeHandler: - - @staticmethod - def send(event): - """Main control center - 1. preprocess the event - 2. Pass the event to update system(s)""" - print(f"{event=}") - - # something changed in scene and it duplicates some tree events which should be ignored - if isinstance(event, ev.SceneEvent): - # this event was caused by update system itself and should be ignored - if 'SKIP_UPDATE' in event.tree: - del event.tree['SKIP_UPDATE'] - return - - was_handled = dict() - # Add update tusk for the tree - for handler in update_systems: - res = handler(event) - was_handled[handler] = res - - if (results := sum(was_handled.values())) > 1: - duplicates = [f.__module__ for f, r in was_handled if r == 1] - raise RuntimeError(f"{event=} was executed more than one time, {duplicates=}") - elif results == 0: - raise RuntimeError(f"{event} was not handled") - - -class ContextTrees: - """It keeps trees with their states""" - _trees: Dict[str, Tree] = dict() - - @classmethod - def get(cls, bl_tree, rebuild=True): - """Return caught tree. If rebuild is true it will try generate new tree if it was not build yet or changed""" - tree = cls._trees.get(bl_tree.tree_id) - - # new tree, all nodes are outdated - if tree is None: - if rebuild: - tree = Tree(bl_tree) - cls._trees[bl_tree.tree_id] = tree - else: - raise RuntimeError(f"Tree={bl_tree} was never executed yet") - - # topology of the tree was changed and should be updated - # Two reasons why always new tree is generated - it's simpler and new tree keeps fresh references to the nodes - elif not tree.is_updated: - if rebuild: - tree = Tree(bl_tree) - cls._update_topology_status(tree) - cls._trees[bl_tree.tree_id] = tree - else: - raise RuntimeError(f"Tree={tree} is outdated") - - return tree - - @classmethod - def mark_tree_outdated(cls, bl_tree): - """Whenever topology of a tree is changed this method should be called.""" - tree = cls._trees.get(bl_tree.tree_id) - if tree: - tree.is_updated = False - - @classmethod - def mark_nodes_outdated(cls, bl_tree, bl_nodes, context=''): - """It will try to mark given nodes as to be recalculated. - If node won't be found status of the tree will be changed to outdated""" - if bl_tree.tree_id not in cls._trees: - return # all nodes will be outdated either way when the tree will be recreated (nothing to do) - - tree = cls._trees[bl_tree.tree_id] - for bl_node in bl_nodes: - try: - if context: - with tree.set_exec_context(context): - tree.nodes[bl_node.name].is_updated = False - else: - del tree.nodes[bl_node.name].is_updated - - # it means that generated tree does no have given node and should be recreated by next request - except KeyError: - tree.is_updated = False - - @classmethod - def reset_data(cls, bl_tree=None): - """ - Should be called upon loading new file, other wise it can lead to errors and even crash - Also according the fact that trees have links to real blender nodes - it is also important to call this method upon undo method otherwise errors and crashes - Also single tree can be added, in this case only it will be deleted - (it's going to be used in force update) - """ - if bl_tree and bl_tree.tree_id in cls._trees: - cls._trees[bl_tree.tree_id].delete() - del cls._trees[bl_tree.tree_id] - else: - for tree in cls._trees.values(): - tree.delete() - cls._trees.clear() - - @classmethod - def calc_cam_update_time(cls, bl_tree, context='') -> dict: - cum_time_nodes = dict() - if bl_tree.tree_id not in cls._trees: - return cum_time_nodes - - tree = cls._trees[bl_tree.tree_id] - with tree.set_exec_context(context): - for node in tree.sorted_walk(tree.output_nodes): - if node.update_time is None: # error node? - cum_time_nodes[node.bl_tween] = None - continue - if len(node.last_nodes) > 1: - cum_time = sum(n.update_time for n in tree.sorted_walk([node]) if n.update_time is not None) - else: - cum_time = sum(cum_time_nodes.get(n.bl_tween, 0) for n in node.last_nodes) + node.update_time - cum_time_nodes[node.bl_tween] = cum_time - return cum_time_nodes - - @classmethod - def calc_cam_update_time_group(cls, bl_tree, group_nodes: List[SvNode]) -> dict: - cum_time_nodes = dict() - if bl_tree.tree_id not in cls._trees: - return cum_time_nodes - - tree = cls._trees[bl_tree.tree_id] - out_nodes = [n for n in tree.nodes if BlNode(n.bl_tween).is_debug_node] - out_nodes.extend([tree.nodes.active_output] if tree.nodes.active_output else []) - for node in tree.sorted_walk(out_nodes): - path = PathManager.generate_path(group_nodes) - with tree.set_exec_context(path): - if node.update_time is None: # error node? - cum_time_nodes[node.bl_tween] = None - continue - if len(node.last_nodes) > 1: - cum_time = sum(n.update_time for n in tree.sorted_walk([node]) if n.update_time is not None) - else: - cum_time = sum(cum_time_nodes.get(n.bl_tween, 0) for n in node.last_nodes) + node.update_time - cum_time_nodes[node.bl_tween] = cum_time - return cum_time_nodes - - @classmethod - def _update_topology_status(cls, new_tree: Tree): - """Copy link node status by comparing with previous tree and save current""" - if new_tree.id in cls._trees: - old_tree = cls._trees[new_tree.id] - - new_links = new_tree.links - old_tree.links - for link in new_links: - if link.from_node.name in old_tree.nodes: - from_old_node = old_tree.nodes[link.from_node.name] - from_old_socket = from_old_node.get_output_socket(link.from_socket.identifier) - has_old_from_socket_links = from_old_socket.links if from_old_socket is not None else False - else: - has_old_from_socket_links = False - - # this is only because some nodes calculated data only if certain output socket is connected - # ideally we would not like to make previous node outdated, but it requires changes in many nodes - if not has_old_from_socket_links: - del link.from_node.is_input_changed - else: - del link.to_node.is_input_changed - - removed_links = old_tree.links - new_tree.links - for link in removed_links: - if link.to_node in new_tree.nodes: - del new_tree.nodes[link.to_node.name].is_input_changed - - -class PathManager: - @staticmethod - def generate_path(group_nodes: List[SvNode]) -> Path: - """path is ordered collection group node ids - max length of path should be no more then number of base trees of most nested group node + 1""" - return Path('.'.join(n.node_id for n in group_nodes)) diff --git a/core/node_group.py b/core/node_group.py index c689e8f89c..5a74be5e04 100644 --- a/core/node_group.py +++ b/core/node_group.py @@ -12,8 +12,8 @@ from typing import Tuple, List, Set, Dict, Iterator, Optional import bpy -from bpy.props import BoolProperty -from sverchok.core.main_tree_handler import TreeHandler +from bpy.props import BoolProperty, EnumProperty +from sverchok.core.event_system import handle_event from sverchok.data_structure import extend_blender_class from mathutils import Vector @@ -33,14 +33,16 @@ class SvGroupTree(SvNodeTreeCommon, bpy.types.NodeTree): bl_icon = 'NODETREE' bl_label = 'Group tree' - handler = TreeHandler - # should be updated by "Go to edit group tree" operator group_node_name: bpy.props.StringProperty(options={'SKIP_SAVE'}) # Always False, does not have sense to have for nested trees, sine of draft mode refactoring sv_draft: bpy.props.BoolProperty(options={'SKIP_SAVE'}) sv_show_time_nodes: BoolProperty(default=False, options={'SKIP_SAVE'}) + show_time_mode: EnumProperty( + items=[(n, n, '') for n in ["Per node", "Cumulative"]], + options={'SKIP_SAVE'}, + ) @classmethod def poll(cls, context): @@ -116,7 +118,7 @@ def update(self): self.check_reroutes_sockets() self.update_sockets() # probably more precise trigger could be found for calling this method - self.handler.send(ev.GroupTreeEvent(self, self.get_update_path())) + handle_event(ev.GroupTreeEvent(self, self.get_update_path())) def update_sockets(self): # todo it lets simplify sockets API """Set properties of sockets of parent nodes and of output modes""" @@ -205,7 +207,7 @@ def update_nodes(self, nodes: list): if not self.group_node_name: # initialization tree return - self.handler.send(ev.GroupPropertyEvent(self, self.get_update_path(), nodes)) + handle_event(ev.GroupPropertyEvent(self, self.get_update_path(), nodes)) def parent_nodes(self) -> Iterator['SvGroupTreeNode']: """Returns all parent nodes""" @@ -293,7 +295,7 @@ def nested_tree_filter(self, context): def update_group_tree(self, context): """Apply filtered tree to `node_tree` attribute. By this attribute Blender is aware of linking between the node and nested tree.""" - TreeHandler.send(ev.TreesGraphEvent()) + handle_event(ev.TreesGraphEvent()) self.node_tree: SvGroupTree = self.group_tree # also default values should be fixed if self.node_tree: @@ -380,6 +382,7 @@ def process(self): # most simple way to pass data about whether node group should show timings self.node_tree.sv_show_time_nodes = self.id_data.sv_show_time_nodes + self.node_tree.show_time_mode = self.id_data.show_time_mode input_node = self.active_input() output_node = self.active_output() @@ -426,10 +429,10 @@ def sv_update(self): n_in_s.default_property_type = t_in_s.default_type def sv_copy(self, original): - TreeHandler.send(ev.TreesGraphEvent()) + handle_event(ev.TreesGraphEvent()) def sv_free(self): - TreeHandler.send(ev.TreesGraphEvent()) + handle_event(ev.TreesGraphEvent()) class PlacingNodeOperator: @@ -811,7 +814,7 @@ def execute(self, context): context.space_data.path.append(sub_tree, node=group_node) sub_tree.group_node_name = group_node.name event = ev.GroupTreeEvent(sub_tree, sub_tree.get_update_path()) - sub_tree.handler.send(event) + handle_event(event) # todo make protection from editing the same trees in more then one area # todo add the same logic to exit from tree operator return {'FINISHED'} diff --git a/core/update_system.py b/core/update_system.py index a02e54d4a5..ad83d32563 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -18,6 +18,7 @@ from sverchok.node_tree import (SverchCustomTreeNode as SvNode, SverchCustomTree as SvTree) +# todo check #4229 UPDATE_KEY = "US_is_updated" ERROR_KEY = "US_error" @@ -221,10 +222,9 @@ def update_animation(cls, event: ev.AnimationEvent): def main_update(cls, tree: NodeTree, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: """Only for main trees 1. Whe it called the tree should have information of what is outdated""" - # todo add cancelling # print(f"UPDATE NODES {event.type=}, {event.tree.name=}") + up_tree = cls.get(tree, refresh_tree=True) if update_nodes: - up_tree = cls.get(tree, refresh_tree=True) walker = up_tree._walk() # walker = up_tree._debug_color(walker) try: @@ -237,7 +237,11 @@ def main_update(cls, tree: NodeTree, update_nodes=True, update_interface=True) - pass if update_interface: - update_ui(tree) + if up_tree._tree.show_time_mode == "Cumulative": + times = up_tree.calc_cam_update_time() + else: + times = None + update_ui(tree, times) @classmethod def reset_tree(cls, tree: NodeTree = None): @@ -259,6 +263,18 @@ def add_outdated(self, nodes: Iterable): if self._outdated_nodes is not None: self._outdated_nodes.update(nodes) + def calc_cam_update_time(self) -> Iterable['SvNode']: + cum_time_nodes = dict() # don't have frame nodes + for node, prev_socks in self.__sort_nodes(): + prev_nodes = self._from_nodes[node] + if len(prev_nodes) > 1: + cum_time = sum(n.get(TIME_KEY, 0) for n in self.nodes_to([node])) + else: + cum_time = sum(cum_time_nodes.get(n, 0) for n in prev_nodes) + cum_time += node.get(TIME_KEY, 0) + cum_time_nodes[node] = cum_time + return (cum_time_nodes.get(n) for n in self._tree.nodes) + def __init__(self, tree: NodeTree): super().__init__(tree) self._tree_catch[tree.tree_id] = self @@ -269,7 +285,7 @@ def __init__(self, tree: NodeTree): self._outdated_nodes: Optional[set[SvNode]] = None # None means outdated all # https://stackoverflow.com/a/68550238 - self._sort_nodes = lru_cache(maxsize=1)(self._sort_nodes) + self._sort_nodes = lru_cache(maxsize=1)(self.__sort_nodes) self._copy_attrs = [ 'is_updated', @@ -315,10 +331,10 @@ def _walk(self) -> tuple[Node, list[NodeSocket]]: else: node[UPDATE_KEY] = False - def _sort_nodes(self, - from_nodes: frozenset['SvNode'] = None, - to_nodes: frozenset['SvNode'] = None)\ - -> list[tuple['SvNode', list[NodeSocket]]]: + def __sort_nodes(self, + from_nodes: frozenset['SvNode'] = None, + to_nodes: frozenset['SvNode'] = None)\ + -> list[tuple['SvNode', list[NodeSocket]]]: nodes_to_walk = set() walk_structure = None if from_nodes is None and to_nodes is None: @@ -429,8 +445,7 @@ def prepare_input_data(prev_socks, input_socks): ns.sv_set(data) -def update_ui(tree: NodeTree): - # todo cumulative time +def update_ui(tree: NodeTree, times: Iterable[float] = None): errors = (n.get(ERROR_KEY, None) for n in tree.nodes) - times = (n.get(TIME_KEY, 0) for n in tree.nodes) + times = times or (n.get(TIME_KEY, 0) for n in tree.nodes) tree.update_ui(errors, times) diff --git a/node_tree.py b/node_tree.py index 006963cba3..8ea275a5c5 100644 --- a/node_tree.py +++ b/node_tree.py @@ -15,7 +15,7 @@ from sverchok.core.sv_custom_exceptions import SvNoDataError import sverchok.core.events as ev -from sverchok.core.main_tree_handler import TreeHandler +from sverchok.core.event_system import handle_event from sverchok.data_structure import classproperty, post_load_call from sverchok.utils import get_node_class_reference from sverchok.utils.sv_node_utils import recursive_framed_location_finder @@ -35,7 +35,13 @@ class SvNodeTreeCommon: name="Node times", default=False, options=set(), - update=lambda s, c: TreeHandler.send(ev.TreeEvent(s))) + update=lambda s, c: handle_event(ev.TreeEvent(s))) + show_time_mode: EnumProperty( + items=[(n, n, '') for n in ["Per node", "Cumulative"]], + options=set(), + update=lambda s, c: handle_event(ev.TreeEvent(s)), + description="Mode of showing node update timings", + ) @property def tree_id(self): @@ -125,17 +131,11 @@ def on_draft_mode_changed(self, context): name="Process", default=True, description='Update upon tree and node property changes', - update=lambda s, c: TreeHandler.send(ev.TreeEvent(s)), + update=lambda s, c: handle_event(ev.TreeEvent(s)), options=set(), ) sv_animate: BoolProperty(name="Animate", default=True, description='Animate this layout', options=set()) sv_show: BoolProperty(name="Show", default=True, description='Show this layout', update=turn_off_ng, options=set()) - show_time_mode: EnumProperty( - items=[(n, n, '') for n in ["Per node", "Cumulative"]], - options=set(), - update=lambda s, c: s.update_ui(), - description="Mode of showing node update timings", - ) sv_show_socket_menus: BoolProperty( name = "Show socket menus", @@ -156,29 +156,29 @@ def on_draft_mode_changed(self, context): def update(self): """This method is called if collection of nodes or links of the tree was changed""" - TreeHandler.send(ev.TreeEvent(self)) + handle_event(ev.TreeEvent(self)) def force_update(self): """Update whole tree from scratch""" # ideally we would never like to use this method but we live in the real world - TreeHandler.send(ev.ForceEvent(self)) + handle_event(ev.ForceEvent(self)) def update_nodes(self, nodes, cancel=True): """This method expects to get list of its nodes which should be updated""" - return TreeHandler.send(ev.PropertyEvent(self, nodes)) + return handle_event(ev.PropertyEvent(self, nodes)) def scene_update(self): """This method should be called by scene changes handler it ignores events related with S sverchok trees in other cases it updates nodes which read data from Blender""" - TreeHandler.send(ev.SceneEvent(self)) + handle_event(ev.SceneEvent(self)) def process_ani(self, frame_changed: bool, animation_playing: bool): """ Process the Sverchok node tree if animation layers show true. For animation callback/handler """ - TreeHandler.send(ev.AnimationEvent(self, frame_changed, animation_playing)) + handle_event(ev.AnimationEvent(self, frame_changed, animation_playing)) class UpdateNodes: diff --git a/settings.py b/settings.py index bcdf9731a7..001972dca7 100644 --- a/settings.py +++ b/settings.py @@ -7,7 +7,6 @@ from bpy.props import BoolProperty, FloatVectorProperty, EnumProperty, IntProperty, FloatProperty, StringProperty from sverchok.dependencies import sv_dependencies, pip, ensurepip, draw_message, get_icon from sverchok import data_structure -from sverchok.core import main_tree_handler # don't remove this should fix #4229 (temp solution) from sverchok.core import handlers from sverchok.utils import logging from sverchok.utils.sv_gist_tools import TOKEN_HELP_URL From a8b432552373839fb362d27f572f57a6f7f5baef Mon Sep 17 00:00:00 2001 From: Durman Date: Tue, 31 May 2022 08:39:54 +0400 Subject: [PATCH 20/25] add support of wifi nodes fix updating tree upon relinking reroutes --- core/update_system.py | 86 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/core/update_system.py b/core/update_system.py index ad83d32563..c53f7107c9 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -104,6 +104,7 @@ def __init__(self, tree: NodeTree): self._links.add((link.from_socket, link.to_socket)) self._remove_reroutes() + self._remove_wifi_nodes() def nodes_from(self, from_nodes: Iterable['SvNode']) -> set['SvNode']: def node_walker_to(node_: 'SvNode'): @@ -158,8 +159,11 @@ def _remove_reroutes(self): continue from_s_node = self._sock_node[from_s] if from_s_node == _node: - from_from_s = self._from_sock.get(from_s_node.inputs[0]) + from_from_s = self._from_sock.get(_node.inputs[0]) + self._links.discard((from_s, sock)) if from_from_s is not None: + self._links.discard((from_from_s, _node.inputs[0])) + self._links.add((from_from_s, sock)) self._from_sock[sock] = from_from_s else: del self._from_sock[sock] @@ -167,7 +171,83 @@ def _remove_reroutes(self): self._from_nodes = {n: k for n, k in self._from_nodes.items() if n.bl_idname != 'NodeReroute'} self._to_nodes = {n: k for n, k in self._to_nodes.items() if n.bl_idname != 'NodeReroute'} - # todo add links between wifi nodes + def _remove_wifi_nodes(self): + wifi_in: dict[str, 'SvNode'] = dict() + wifi_out: dict[str, set['SvNode']] = defaultdict(set) + for node in self._tree.nodes: + if var := getattr(node, 'var_name', ''): + if node.bl_idname == 'WifiInNode': + wifi_in[var] = node + elif node.bl_idname == 'WifiOutNode': + wifi_out[var].add(node) + + to_socks: dict[NodeSocket, set[NodeSocket]] = defaultdict(set) + for link in (li for li in self._tree.links if not li.is_muted): + to_socks[link.from_socket].add(link.to_socket) + + for var, in_ in wifi_in.items(): + for out in wifi_out[var]: + for in_sock, out_sock in zip(in_.inputs, out.outputs): + if from_s := self._from_sock.get(in_sock): + from_n = self._sock_node[from_s] + self._to_nodes[from_n].discard(in_) + del self._from_sock[in_sock] + self._links.discard((from_s, in_sock)) + if to_ss := to_socks.get(out_sock): + for to_s in to_ss: + to_n = self._sock_node[to_s] + self._from_nodes[to_n].discard(out) + self._links.discard((out_sock, to_s)) + if from_s and to_ss: + for to_s in to_ss: + to_n = self._sock_node[to_s] + self._from_nodes[to_n].add(from_n) + self._to_nodes[from_n].add(to_n) + self._from_sock[to_s] = from_s + self._links.add((from_s, to_s)) + + self._from_nodes = {n: k for n, k in self._from_nodes.items() + if n.bl_idname not in {'WifiInNode', 'WifiOutNode'}} + self._to_nodes = {n: k for n, k in self._to_nodes.items() + if n.bl_idname not in {'WifiInNode', 'WifiOutNode'}} + + def __repr__(self): + def from_nodes_str(): + for tn, fns in self._from_nodes.items(): + yield f" {tn.name}" + for fn in fns: + yield f" {fn.name}" + + def to_nodes_str(): + for fn, tns in self._to_nodes.items(): + yield f" {fn.name}" + for tn in tns: + yield f" {tn.name}" + + def from_sock_str(): + for tso, fso in self._from_sock.items(): + yield f" From='{fso.node.name}|{fso.name}'" \ + f" to='{tso.node.name}|{tso.name}'" + + def links_str(): + for from_, to in self._links: + yield f" From='{from_.node.name}|{from_.name}'" \ + f" to='{to.node.name}|{to.name}'" + + from_nodes = "\n".join(from_nodes_str()) + to_nodes = "\n".join(to_nodes_str()) + from_sock = "\n".join(from_sock_str()) + links = "\n".join(links_str()) + msg = f"<{type(self).__name__}\n" \ + f"from_nodes:\n" \ + f"{from_nodes}\n" \ + f"to_nodes:\n" \ + f"{to_nodes}\n" \ + f"from sockets:\n" \ + f"{from_sock}\n" \ + f"links:\n" \ + f"{links}" + return msg class UpdateTree(SearchTree): @@ -320,7 +400,7 @@ def _walk(self) -> tuple[Node, list[NodeSocket]]: # walk triggered nodes and error nodes from previous updates else: outdated = frozenset(self._outdated_nodes) - self._outdated_nodes.clear() # todo what if execution was canceled? + self._outdated_nodes.clear() for node, other_socks in self._sort_nodes(outdated): # execute node only if all previous nodes are updated From 9c6c2e6aec6ad53e94b29609e61ddd2c2c7a6da7 Mon Sep 17 00:00:00 2001 From: Durman Date: Tue, 31 May 2022 08:42:14 +0400 Subject: [PATCH 21/25] increment Sverchok version --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index d7c2e07028..f905becf53 100755 --- a/__init__.py +++ b/__init__.py @@ -53,7 +53,7 @@ "tracker_url": "http://www.blenderartists.org/forum/showthread.php?272679" } -VERSION = 'v1.1.0-alpha.1' # looks like the only way to have custom format for the version +VERSION = 'v1.1.0-alpha.2' # looks like the only way to have custom format for the version import sys import importlib From 4135186655a9d5005f101a52586b519f5262b6bb Mon Sep 17 00:00:00 2001 From: Durman Date: Tue, 31 May 2022 16:31:16 +0400 Subject: [PATCH 22/25] fix tests which read data from input sockets without passing the data from output sockets first --- core/socket_data.py | 2 +- tests/json_import_tests.py | 4 ++++ tests/simple_viewer_text_tests.py | 1 + tests/socket_conversion_tests.py | 4 +++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/socket_data.py b/core/socket_data.py index a169f385f4..f6a0fb5633 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -194,7 +194,7 @@ def get_output_socket_data(node, output_socket_name): Get data that the node has written to the output socket. Raises SvNoDataError if it hasn't written any. """ - socket = node.inputs[output_socket_name] # todo why output? + socket = node.outputs[output_socket_name] sock_address = socket.socket_id if sock_address in socket_data_cache: return socket_data_cache[sock_address] diff --git a/tests/json_import_tests.py b/tests/json_import_tests.py index fee78aa650..3463a85d9c 100644 --- a/tests/json_import_tests.py +++ b/tests/json_import_tests.py @@ -1,3 +1,4 @@ +from sverchok.old_nodes import is_old from sverchok.utils.dummy_nodes import is_dependent from sverchok.utils.testing import * from sverchok.ui.sv_examples_menu import example_categories_names @@ -7,6 +8,7 @@ class ScriptUvImportTest(SverchokTestCase): def test_script_uv_import(self): with self.temporary_node_tree("ImportedTree") as new_tree: + new_tree.sv_process = False path = self.get_reference_file_path("script_uv.json") importer = JSONImporter.init_from_path(path) importer.import_into_tree(new_tree, print_log=False) @@ -27,6 +29,7 @@ class ProfileImportTest(SverchokTestCase): def test_profile_import(self): with self.temporary_node_tree("ImportedTree") as new_tree: + new_tree.sv_process = False importer = JSONImporter.init_from_path(self.get_reference_file_path("profile.json")) importer.import_into_tree(new_tree, print_log=False) if importer.has_fails: @@ -37,6 +40,7 @@ class MeshExprImportTest(SverchokTestCase): def test_mesh_expr_import(self): with self.temporary_node_tree("ImportedTree") as new_tree: + new_tree.sv_process = False importer = JSONImporter.init_from_path(self.get_reference_file_path("mesh.json")) importer.import_into_tree(new_tree, print_log=False) if importer.has_fails: diff --git a/tests/simple_viewer_text_tests.py b/tests/simple_viewer_text_tests.py index f156a267a0..0f801e27a1 100644 --- a/tests/simple_viewer_text_tests.py +++ b/tests/simple_viewer_text_tests.py @@ -16,6 +16,7 @@ def test_text_viewer(self): # Trigger processing of Cylinder node cyl.process() + viewer_text.inputs[0].sv_set(cyl.outputs['Edges'].sv_get()) # Invoke "VIEW" operator bpy.ops.node.sverchok_viewer_buttonmk1('EXEC_DEFAULT', treename=self.tree.name, nodename=viewer_text.name) diff --git a/tests/socket_conversion_tests.py b/tests/socket_conversion_tests.py index 76b945ca07..3bdc193b3f 100644 --- a/tests/socket_conversion_tests.py +++ b/tests/socket_conversion_tests.py @@ -1,4 +1,4 @@ - +from sverchok.core.update_system import prepare_input_data from mathutils import Matrix from sverchok.core.socket_conversions import ImplicitConversionProhibited from sverchok.utils.testing import * @@ -19,6 +19,7 @@ def test_vertices_to_matrices(self): # Trigger processing of NGon node ngon.process() + prepare_input_data([ngon.outputs['Vertices']], [matrix_apply.inputs['Matrixes']]) # Read what MatrixApply node sees data =[[v[:] for v in m] for m in matrix_apply.inputs['Matrixes'].sv_get()] @@ -114,6 +115,7 @@ def test_adaptive_sockets(self): # We do not actually care about the data # itself, it is only important that there # was no exception. + node.inputs[input_name].sv_set(ngon.outputs['Vertices'].sv_get()) data = node.inputs[input_name].sv_get() except ImplicitConversionProhibited as e: raise e From 20704e6c0b4ea9060a103d63b91e13fbf0b52704 Mon Sep 17 00:00:00 2001 From: Durman Date: Wed, 1 Jun 2022 13:19:58 +0400 Subject: [PATCH 23/25] add code documentation --- core/events.py | 21 ++++--- core/group_update_system.py | 48 +++++++++++--- core/tasks.py | 36 +++++++++-- core/update_system.py | 121 +++++++++++++++++++++++++++--------- nodes/logic/evolver.py | 2 +- nodes/logic/loop_out.py | 4 +- 6 files changed, 175 insertions(+), 57 deletions(-) diff --git a/core/events.py b/core/events.py index 928017d718..9c82dee3b8 100644 --- a/core/events.py +++ b/core/events.py @@ -6,12 +6,8 @@ # License-Filename: LICENSE """ -Purpose of this module is centralization of update events. - -For now it can be used in debug mode for understanding which event method are triggered by Blender -during evaluation of Python code. - -Details: https://github.com/nortikin/sverchok/issues/3077 +Events keep information about which Blender trigger was executed and with which +context """ from __future__ import annotations @@ -29,8 +25,8 @@ class TreeEvent: - """Keeps information about what was changed during the even""" - # task should be run via timer only https://developer.blender.org/T82318#1053877 + """Adding removing nodes or links but not necessarily""" + # the event should be run via timer only https://developer.blender.org/T82318#1053877 tree: SvTree def __init__(self, tree): @@ -41,10 +37,12 @@ def __repr__(self): class ForceEvent(TreeEvent): + """Indicates the whole tree should be recalculated""" pass class AnimationEvent(TreeEvent): + """Frame was changed. Last event can be with the same frame""" is_frame_changed: bool is_animation_playing: bool @@ -55,10 +53,12 @@ def __init__(self, tree, is_frame_change, is_animation_laying): class SceneEvent(TreeEvent): + """Something was changed in the scene""" pass class PropertyEvent(TreeEvent): + """Property of the node(s) was changed""" updated_nodes: Iterable[SvNode] def __init__(self, tree, updated_nodes): @@ -67,6 +67,7 @@ def __init__(self, tree, updated_nodes): class GroupTreeEvent(TreeEvent): + """The same as Tree event but inside a group tree""" tree: GrTree update_path: list[GrNode] @@ -76,6 +77,7 @@ def __init__(self, tree, update_path): class GroupPropertyEvent(GroupTreeEvent): + """Property of a node(s) inside a group tree was changed""" updated_nodes: Iterable[SvNode] def __init__(self, tree, update_path, update_nodes): @@ -84,8 +86,11 @@ def __init__(self, tree, update_path, update_nodes): class FileEvent: + """It indicates that new file was loaded""" pass class TreesGraphEvent: + """It indicates that something was changed in trees relations defined via + group nodes""" pass diff --git a/core/group_update_system.py b/core/group_update_system.py index 09767d67db..a73720d28d 100644 --- a/core/group_update_system.py +++ b/core/group_update_system.py @@ -20,11 +20,10 @@ def control_center(event): 1. Update tree model lazily 2. Check whether the event should be processed 3. Process event or create task to process via timer""" - was_executed = False + was_executed = True # property of some node of a group tree was changed if type(event) is ev.GroupPropertyEvent: - was_executed = True gr_tree = GroupUpdateTree.get(event.tree) gr_tree.add_outdated(event.updated_nodes) gr_tree.update_path = event.update_path @@ -35,10 +34,9 @@ def control_center(event): # topology of a group tree was changed elif type(event) is ev.GroupTreeEvent: - was_executed = True gr_tree = GroupUpdateTree.get(event.tree) gr_tree.is_updated = False - # gr_tree.update_path = event.update_path + gr_tree.update_path = event.update_path for main_tree in trees_graph[event.tree]: us.UpdateTree.get(main_tree).add_outdated(trees_graph[main_tree, event.tree]) if main_tree.sv_process: @@ -46,16 +44,25 @@ def control_center(event): # Connections between trees were changed elif type(event) is ev.TreesGraphEvent: - was_executed = True trees_graph.is_updated = False + else: + was_executed = False + return was_executed class GroupUpdateTree(us.UpdateTree): - get: Callable[['GrTree'], 'GroupUpdateTree'] + """Group trees has their own update method separate from main tree to have + more nice profiling statistics. Also, it keeps some specific to grop trees + statuses.""" + get: Callable[['GrTree'], 'GroupUpdateTree'] # type hinting does not work grate :/ def update(self, node: 'GrNode'): + """Updates outdated nodes of group tree. Also, it keeps proper state of + the exec_path. If exec_path is equal to update path it also updates UI + of the tree + :node: group node which tree is executed""" self._exec_path.append(node) try: is_opened_tree = self.update_path == self._exec_path @@ -71,7 +78,7 @@ def update(self, node: 'GrNode'): if is_opened_tree: if self._tree.show_time_mode == "Cumulative": - times = self.calc_cam_update_time() + times = self._calc_cam_update_time() else: times = None us.update_ui(self._tree, times) @@ -82,6 +89,13 @@ def update(self, node: 'GrNode'): self._exec_path.pop() def __init__(self, tree): + """Should node be used directly but wia the get class method + :update_path: list of group nodes via which update trigger was executed + :_exec_path: list of group nodes via which the tree is executed + :_viewer_nodes: output nodes which should be updated. If not presented + all output nodes will be updated. The main reason of having them is to + update viewer nodes only in opened group tree, as a side effect it + optimises nodes execution""" super().__init__(tree) # update UI for the tree opened under the given path self.update_path: list['GrNode'] = [] @@ -94,6 +108,11 @@ def __init__(self, tree): self._copy_attrs.extend(['_exec_path', 'update_path', '_viewer_nodes']) def _walk(self) -> tuple[Node, list[NodeSocket]]: + """Yields nodes in order of their proper execution. It starts yielding + from outdated nodes. It keeps the outdated_nodes storage in proper + state. It checks after yielding the error status of the node. If the + node has error it goes into outdated_nodes. If tree has viewer nodes + it yields only nodes which should be called to update viewers.""" # walk all nodes in the tree if self._outdated_nodes is None: outdated = None @@ -104,7 +123,7 @@ def _walk(self) -> tuple[Node, list[NodeSocket]]: else: outdated = frozenset(self._outdated_nodes) viewers = frozenset(self._viewer_nodes) - self._outdated_nodes.clear() # todo what if execution was canceled? + self._outdated_nodes.clear() self._viewer_nodes.clear() for node, other_socks in self._sort_nodes(outdated, viewers): @@ -118,10 +137,17 @@ def _walk(self) -> tuple[Node, list[NodeSocket]]: class TreesGraph: + """It keeps relationships between main trees and group trees.""" _group_main: dict['GrTree', set['SvTree']] _entry_nodes: dict['SvTree', dict['GrTree', set['SvNode']]] def __init__(self): + """:is_updated: the graph can be marked as outdated in this case it will + be updated automatically whenever data will be fetched from it + :_group_main: it stores information about in which main trees a group + tree is used. The group tree can be located in some nested groups too + :_entry_nodes: it stores information about which group nodes in main + tree should be called to update a group tree""" self.is_updated = False self._group_main = defaultdict(set) @@ -133,7 +159,8 @@ def __getitem__(self, item: 'GrTree') -> set['SvTree']: ... def __getitem__(self, item: tuple['SvTree', 'GrTree']) -> set['SvNode']: ... def __getitem__(self, item): - # print(self) + """It either returns related to given group tree Main tree or collection + of group nodes to update given group tree""" if not self.is_updated: self._update() if isinstance(item, tuple): @@ -143,7 +170,7 @@ def __getitem__(self, item): return self._group_main[item] def _update(self): - # print("REFRESH TreesGraph") + """Calculate relationships between group trees and main trees""" self._group_main.clear() self._entry_nodes.clear() for tree in BlTrees().sv_main_trees: @@ -154,6 +181,7 @@ def _update(self): @staticmethod def _walk(from_: NodeTree) -> Iterator[tuple[NodeTree, 'SvNode']]: + """Iterate over all nested node trees""" current_entry_node = None def next_(_tree): diff --git a/core/tasks.py b/core/tasks.py index ba8085af8f..a75d801b6f 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -16,6 +16,7 @@ class Tasks: """ + It keeps tasks which should be executed and executes the on demand. 1. Execute tasks 2. Time the whole execution 3. Display the progress in the UI @@ -24,17 +25,23 @@ class Tasks: _current: Optional['Task'] def __init__(self): + """:_todo: list of tasks to run + :_current: task which was started to execute""" self._todo = set() self._current = None def __bool__(self): + """Has anything to do?""" return bool(self._current or self._todo) def add(self, task: 'Task'): + """Add new tasks to run them via timer""" self._todo.add(task) @profile(section="UPDATE") def run(self): + """Run given tasks to update trees and report execution process in the + header of a node tree editor""" max_duration = 0.15 # 0.15 is max timer frequency duration = 0 @@ -52,6 +59,7 @@ def run(self): self._finish() def cancel(self): + """Remove all tasks in the queue and abort current one""" self._todo.clear() if self._current: try: @@ -63,6 +71,8 @@ def cancel(self): @property def current(self) -> Optional['Task']: + """Return current task if it is absent it tries to pop it from the tasks + queue if it's empty returns None""" if self._current: return self._current elif self._todo: @@ -73,15 +83,19 @@ def current(self) -> Optional['Task']: return None def _start(self): + """Preprocessing before executing the whole queue of events""" self._start_time gc.disable() # for performance def _next(self): + """Should be called to switch to next tasks when current is exhausted + It made some cleanups after the previous task""" self._report_progress() self._current = self._todo.pop() if self._todo else None del self._main_area def _finish(self): + """Cleanups. Also triggers scene handler and mark trees to skip it""" self._report_progress() del self._main_area @@ -100,6 +114,7 @@ def _finish(self): @cached_property def _start_time(self): + """Start time of execution the whole queue of tasks""" return time() @cached_property @@ -114,6 +129,8 @@ def _main_area(self) -> Optional: return area def _report_progress(self, text: str = None): + """Show text in the tree editor header. If text is none the header + returns in its initial condition""" if self._main_area: self._main_area.header_text_set(text) @@ -133,7 +150,15 @@ def tree_event_loop(delay): class Task: + """Generator which should update some node tree. The task is hashable, and + it is equal to another task if booth of them update the same tree. + The generator is suspendable and can limit its execution by given time""" def __init__(self, tree, updater): + """:tree: tree which should be updated + :_updater: generator which should update given tree + :is_exhausted: the status of the generator - read only + :last_node: last node which going to be processed by the generator + - read only""" self.tree: SvTree = tree self.is_exhausted = False self.last_node = None @@ -142,6 +167,9 @@ def __init__(self, tree, updater): self.__hash__ = cache(self.__hash__) def run(self, max_duration): + """Starts the tree updating + :max_duration: if updating of the tree takes more time than given + maximum duration it saves its state and returns execution flow""" duration = 0 try: start_time = time() @@ -154,14 +182,12 @@ def run(self, max_duration): self.is_exhausted = True return duration - def throw(self, error): + def throw(self, error: CancelError): + """Should be used to cansel tree execution. Updater should add + the error to current node and abort the execution""" self._updater.throw(error) self.is_exhausted = True - @property - def id(self): - return self.tree.tree_id - def __eq__(self, other: 'Task'): return self.tree.tree_id == other.tree.tree_id diff --git a/core/update_system.py b/core/update_system.py index c53f7107c9..d32cda1b79 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -30,28 +30,25 @@ def control_center(event): 1. Update tree model lazily 2. Check whether the event should be processed 3. Process event or create task to process via timer""" - was_executed = False + was_executed = True # frame update # This event can't be handled via NodesUpdater during animation rendering # because new frame change event can arrive before timer finishes its tusk. # Or timer can start working before frame change is handled. if type(event) is ev.AnimationEvent: - was_executed = True if event.tree.sv_animate: UpdateTree.get(event.tree).is_animation_updated = False UpdateTree.update_animation(event) # something changed in the scene elif type(event) is ev.SceneEvent: - was_executed = True if event.tree.sv_scene_update and event.tree.sv_process: UpdateTree.get(event.tree).is_scene_updated = False ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) # nodes changed properties elif type(event) is ev.PropertyEvent: - was_executed = True tree = UpdateTree.get(event.tree) tree.add_outdated(event.updated_nodes) if event.tree.sv_process: @@ -59,26 +56,29 @@ def control_center(event): # update the whole tree anyway elif type(event) is ev.ForceEvent: - was_executed = True UpdateTree.reset_tree(event.tree) ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) # mark that the tree topology has changed elif type(event) is ev.TreeEvent: - was_executed = True UpdateTree.get(event.tree).is_updated = False if event.tree.sv_process: ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) # new file opened elif type(event) is ev.FileEvent: - was_executed = True UpdateTree.reset_tree() + else: + was_executed = False return was_executed class SearchTree: + """Data structure which represents Blender node trees but with ability + of efficient search tree elements. Also it keeps tree state so it can be + compared with new one to define differences.""" + _from_nodes: dict['SvNode', set['SvNode']] _to_nodes: dict['SvNode', set['SvNode']] _from_sock: dict[NodeSocket, NodeSocket] @@ -107,6 +107,7 @@ def __init__(self, tree: NodeTree): self._remove_wifi_nodes() def nodes_from(self, from_nodes: Iterable['SvNode']) -> set['SvNode']: + """Returns all next nodes from given ones""" def node_walker_to(node_: 'SvNode'): for nn in self._to_nodes.get(node_, []): yield nn @@ -114,6 +115,7 @@ def node_walker_to(node_: 'SvNode'): return set(bfs_walk(from_nodes, node_walker_to)) def nodes_to(self, to_nodes: Iterable['SvNode']) -> set['SvNode']: + """Returns all previous nodes from given ones""" def node_walker_from(node_: 'SvNode'): for nn in self._from_nodes.get(node_, []): yield nn @@ -121,6 +123,7 @@ def node_walker_from(node_: 'SvNode'): return set(bfs_walk(to_nodes, node_walker_from)) def sort_nodes(self, nodes: Iterable['SvNode']) -> list['SvNode']: + """Returns nodes in order of their correct execution""" walk_structure: dict[SvNode, set[SvNode]] = defaultdict(set) for n in nodes: if n in self._from_nodes: @@ -131,11 +134,17 @@ def sort_nodes(self, nodes: Iterable['SvNode']) -> list['SvNode']: nodes.append(node) return nodes - def previous_sockets(self, node: 'SvNode') -> list[NodeSocket]: + def previous_sockets(self, node: 'SvNode') -> list[Optional[NodeSocket]]: + """Return output sockets connected to input ones of given node + If input socket is not linked the output socket will be None""" return [self._from_sock.get(s) for s in node.inputs] - def update_node(self, node: 'SvNode', supress=True): - with AddStatistic(node, supress): + def update_node(self, node: 'SvNode', suppress=True): + """Fetches data from previous node, makes data conversion if connected + sockets have different types, calls process method of the given node + records nodes statistics + If suppress is True an error during node execution will be suppressed""" + with AddStatistic(node, suppress): prepare_input_data(self.previous_sockets(node), node.inputs) node.process() @@ -251,15 +260,16 @@ def links_str(): class UpdateTree(SearchTree): - """It catches some data for more efficient searches compare to Blender - tree data structure""" + """It caches the trees to keep outdated nodes and to perform tree updating + efficiently.""" _tree_catch: dict[str, 'UpdateTree'] = dict() # the module should be auto-reloaded to prevent crashes @classmethod def get(cls, tree: "SvTree", refresh_tree=False) -> "UpdateTree": """ + Get cached tree. If tree was not cached it will be. :refresh_tree: if True it will convert update flags into outdated - nodes. This can be expensive so it should be called only before tree + nodes. This can be expensive, so it should be called only before tree reevaluation """ if tree.tree_id not in cls._tree_catch: @@ -291,6 +301,7 @@ def get(cls, tree: "SvTree", refresh_tree=False) -> "UpdateTree": @classmethod @profile(section="UPDATE") def update_animation(cls, event: ev.AnimationEvent): + """Should be called to updated animated nodes""" try: g = cls.main_update(event.tree, event.is_frame_changed, not event.is_animation_playing) while True: @@ -300,8 +311,11 @@ def update_animation(cls, event: ev.AnimationEvent): @classmethod def main_update(cls, tree: NodeTree, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: - """Only for main trees - 1. Whe it called the tree should have information of what is outdated""" + """Thi generator is for the triggers. It can update outdated nodes and + update UI. Should be used only with main trees, the group trees should + use different method to separate profiling statistics. Whe it called the + tree should have information of what is outdated""" + # print(f"UPDATE NODES {event.type=}, {event.tree.name=}") up_tree = cls.get(tree, refresh_tree=True) if update_nodes: @@ -318,14 +332,14 @@ def main_update(cls, tree: NodeTree, update_nodes=True, update_interface=True) - if update_interface: if up_tree._tree.show_time_mode == "Cumulative": - times = up_tree.calc_cam_update_time() + times = up_tree._calc_cam_update_time() else: times = None update_ui(tree, times) @classmethod def reset_tree(cls, tree: NodeTree = None): - """Remove tree data or data of all trees""" + """Remove tree data or data of all trees from the cache""" if tree is not None and tree.tree_id in cls._tree_catch: del cls._tree_catch[tree.tree_id] else: @@ -333,29 +347,29 @@ def reset_tree(cls, tree: NodeTree = None): def copy(self) -> 'UpdateTree': """They copy will be with new topology if original tree was changed - since berth of the first tree. Other attributes copied as is.""" + since instancing of the first tree. Other attributes copied as is.""" copy_ = type(self)(self._tree) for attr in self._copy_attrs: setattr(copy_, attr, copy(getattr(self, attr))) return copy_ def add_outdated(self, nodes: Iterable): + """Add outdated nodes explicitly. Animation and scene dependent nodes + can be marked as outdated via dedicated flags for performance.""" if self._outdated_nodes is not None: self._outdated_nodes.update(nodes) - def calc_cam_update_time(self) -> Iterable['SvNode']: - cum_time_nodes = dict() # don't have frame nodes - for node, prev_socks in self.__sort_nodes(): - prev_nodes = self._from_nodes[node] - if len(prev_nodes) > 1: - cum_time = sum(n.get(TIME_KEY, 0) for n in self.nodes_to([node])) - else: - cum_time = sum(cum_time_nodes.get(n, 0) for n in prev_nodes) - cum_time += node.get(TIME_KEY, 0) - cum_time_nodes[node] = cum_time - return (cum_time_nodes.get(n) for n in self._tree.nodes) - def __init__(self, tree: NodeTree): + """Should not use be used directly, only via the get class method + :is_updated: Should be False if topology of the tree was changed + :is_animation_updated: Should be False animation dependent nodes should + be updated + :is_scene_updated: Should be False if scene dependent nodes should be + updated + :_outdated_nodes: Keeps nodes which properties were changed or which + have errors. Can be None when what means that all nodes are outdated + :_copy_attrs: list of attributes which should be copied by the copy + method""" super().__init__(tree) self._tree_catch[tree.tree_id] = self @@ -375,6 +389,7 @@ def __init__(self, tree: NodeTree): ] def _animation_nodes(self) -> set['SvNode']: + """Returns nodes which are animation dependent""" an_nodes = set() if not self.is_animation_updated: for node in self._tree.nodes: @@ -384,6 +399,7 @@ def _animation_nodes(self) -> set['SvNode']: return an_nodes def _scene_nodes(self) -> set['SvNode']: + """Returns nodes which are scene dependent""" sc_nodes = set() if not self.is_scene_updated: for node in self._tree.nodes: @@ -393,6 +409,13 @@ def _scene_nodes(self) -> set['SvNode']: return sc_nodes def _walk(self) -> tuple[Node, list[NodeSocket]]: + """Yields nodes in order of their proper execution. It starts yielding + from outdated nodes. It keeps the outdated_nodes storage in proper + state. It checks after yielding the error status of the node. If the + node has error it goes into outdated_nodes. It uses cached walker, so + it works more efficient when outdated nodes are the same between the + method calls.""" + # walk all nodes in the tree if self._outdated_nodes is None: outdated = None @@ -415,6 +438,12 @@ def __sort_nodes(self, from_nodes: frozenset['SvNode'] = None, to_nodes: frozenset['SvNode'] = None)\ -> list[tuple['SvNode', list[NodeSocket]]]: + """Sort nodes of the tree in proper execution order. Whe all given + parameters are None it uses all tree nodes + :from_nodes: if given it sorts only next nodes from given ones + :to_nodes: if given it sorts only previous nodes from given + If from_nodes and to_nodes are given it uses only intersection of next + nodes from from_nodes and previous nodes from to_nodes""" nodes_to_walk = set() walk_structure = None if from_nodes is None and to_nodes is None: @@ -442,6 +471,9 @@ def __sort_nodes(self, return nodes def _update_difference(self, old: 'UpdateTree') -> set['SvNode']: + """Returns nodes which should be updated according to changes in the + tree topology + :old: previous state of the tree to compare with""" nodes_to_update = self._from_nodes.keys() - old._from_nodes.keys() new_links = self._links - old._links for from_sock, to_sock in new_links: @@ -455,7 +487,22 @@ def _update_difference(self, old: 'UpdateTree') -> set['SvNode']: nodes_to_update.add(old._sock_node[to_sock]) return nodes_to_update + def _calc_cam_update_time(self) -> Iterable['SvNode']: + """Return cumulative update time in order of node_group.nodes collection""" + cum_time_nodes = dict() # don't have frame nodes + for node, prev_socks in self.__sort_nodes(): + prev_nodes = self._from_nodes[node] + if len(prev_nodes) > 1: + cum_time = sum(n.get(TIME_KEY, 0) for n in self.nodes_to([node])) + else: + cum_time = sum(cum_time_nodes.get(n, 0) for n in prev_nodes) + cum_time += node.get(TIME_KEY, 0) + cum_time_nodes[node] = cum_time + return (cum_time_nodes.get(n) for n in self._tree.nodes) + def _debug_color(self, walker: Generator, use_color: bool = True): + """Colorize nodes which were previously executed. Before execution, it + resets all dbug colors""" def _set_color(node: 'SvNode', _use_color: bool): use_key = "DEBUG_use_user_color" color_key = "DEBUG_user_color" @@ -485,9 +532,14 @@ def _set_color(node: 'SvNode', _use_color: bool): class AddStatistic: + """It caches errors during execution of process method of a node and saves + update time, update status and error""" + + # this probably can be inside the Node class as an update method # using context manager from contextlib has big overhead # https://stackoverflow.com/questions/26152934/why-the-staggering-overhead-50x-of-contextlib-and-the-with-statement-in-python def __init__(self, node: 'SvNode', supress=True): + """:supress: if True any errors during node execution will be suppressed""" self._node = node self._start = perf_counter() self._supress = supress @@ -511,7 +563,11 @@ def __exit__(self, exc_type, exc_val, exc_tb): return issubclass(exc_type, Exception) -def prepare_input_data(prev_socks, input_socks): +def prepare_input_data(prev_socks: list[Optional[NodeSocket]], + input_socks: list[NodeSocket]): + """Reads data from given outputs socket make it conversion if necessary and + put data into input given socket""" + # this can be a socket method for ps, ns in zip(prev_socks, input_socks): if ps is None: continue @@ -526,6 +582,9 @@ def prepare_input_data(prev_socks, input_socks): def update_ui(tree: NodeTree, times: Iterable[float] = None): + """Updates UI of the given tree + :times: optional node timing in order of group_tree.nodes collection""" + # probably this can be moved to tree.update_ui method errors = (n.get(ERROR_KEY, None) for n in tree.nodes) times = times or (n.get(TIME_KEY, 0) for n in tree.nodes) tree.update_ui(errors, times) diff --git a/nodes/logic/evolver.py b/nodes/logic/evolver.py index e776f77073..13dbe95773 100644 --- a/nodes/logic/evolver.py +++ b/nodes/logic/evolver.py @@ -365,7 +365,7 @@ def evaluate_fitness(self, tree, node, s_tree: UpdateTree, exec_order): tree.sv_process = True for node in exec_order: try: - s_tree.update_node(node, supress=False) + s_tree.update_node(node, suppress=False) except Exception: raise diff --git a/nodes/logic/loop_out.py b/nodes/logic/loop_out.py index bef8344528..5c014fc308 100644 --- a/nodes/logic/loop_out.py +++ b/nodes/logic/loop_out.py @@ -251,7 +251,7 @@ def for_each_mode(self, loop_in_node): print(f"Looping Object Number {idx}") for node in sort_loop_nodes[1:-1]: try: - tree.update_node(node, supress=False) + tree.update_node(node, suppress=False) except Exception: raise Exception(f"Element: {idx}") @@ -308,7 +308,7 @@ def range_mode(self, loop_in_node): print(f"Looping iteration Number {i+1}") for node in sort_loop_nodes[1:-1]: try: - tree.update_node(node, supress=False) + tree.update_node(node, suppress=False) except Exception: raise Exception(f"Iteration number: {i+1}") From 5b198c89b8e33aa9c78e3641adad1f8f5056643a Mon Sep 17 00:00:00 2001 From: Durman Date: Wed, 1 Jun 2022 14:24:53 +0400 Subject: [PATCH 24/25] fix importing problem in ugly way for now --- core/update_system.py | 2 -- settings.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/update_system.py b/core/update_system.py index d32cda1b79..5b218cbca8 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -18,8 +18,6 @@ from sverchok.node_tree import (SverchCustomTreeNode as SvNode, SverchCustomTree as SvTree) -# todo check #4229 - UPDATE_KEY = "US_is_updated" ERROR_KEY = "US_error" TIME_KEY = "US_time" diff --git a/settings.py b/settings.py index 001972dca7..e084deb73e 100644 --- a/settings.py +++ b/settings.py @@ -7,6 +7,7 @@ from bpy.props import BoolProperty, FloatVectorProperty, EnumProperty, IntProperty, FloatProperty, StringProperty from sverchok.dependencies import sv_dependencies, pip, ensurepip, draw_message, get_icon from sverchok import data_structure +from sverchok.core import tasks # don't remove this should fix #4229 (temp solution) from sverchok.core import handlers from sverchok.utils import logging from sverchok.utils.sv_gist_tools import TOKEN_HELP_URL From ff52da0003662689f32cdd95331eea42e657ab91 Mon Sep 17 00:00:00 2001 From: Durman Date: Wed, 1 Jun 2022 15:47:25 +0400 Subject: [PATCH 25/25] fix handling undo event --- core/handlers.py | 8 -------- core/update_system.py | 13 +++++++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/core/handlers.py b/core/handlers.py index 0ab31480d2..360952deae 100644 --- a/core/handlers.py +++ b/core/handlers.py @@ -84,14 +84,6 @@ def sv_handler_undo_post(scene): undo_handler_node_count['sv_groups'] = 0 - # ideally we would like to recalculate all from scratch - # but with heavy trees user can be scared of pressing undo button - # I consider changes in tree topology as most common case - # but if properties or work of some viewer node (removing generated objects) was effected by undo - # only recalculating of all can restore the adequate state of a tree - for tree in BlTrees().sv_main_trees: - tree.update() # the tree could changed by undo event - @persistent def sv_update_handler(scene): diff --git a/core/update_system.py b/core/update_system.py index 5b218cbca8..1477df4fd8 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -58,6 +58,9 @@ def control_center(event): ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) # mark that the tree topology has changed + # also this can be called (by Blender) during undo event in this case all + # nodes will have another hash id and the comparison method will decide that + # all nodes are new, and won't be able to detect changes, and will update all elif type(event) is ev.TreeEvent: UpdateTree.get(event.tree).is_updated = False if event.tree.sv_process: @@ -279,7 +282,7 @@ def get(cls, tree: "SvTree", refresh_tree=False) -> "UpdateTree": # update topology if not _tree.is_updated: old = _tree - _tree = old.copy() + _tree = old.copy(tree) # update outdated nodes list if _tree._outdated_nodes is not None: @@ -343,10 +346,12 @@ def reset_tree(cls, tree: NodeTree = None): else: cls._tree_catch.clear() - def copy(self) -> 'UpdateTree': + def copy(self, new_tree: NodeTree) -> 'UpdateTree': """They copy will be with new topology if original tree was changed - since instancing of the first tree. Other attributes copied as is.""" - copy_ = type(self)(self._tree) + since instancing of the first tree. Other attributes copied as is. + :new_tree: it's import to pass fresh tree object because during undo + events all previous tree objects invalidates""" + copy_ = type(self)(new_tree) for attr in self._copy_attrs: setattr(copy_, attr, copy(getattr(self, attr))) return copy_