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 diff --git a/core/__init__.py b/core/__init__.py index 62c4333d4f..5f0e696089 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 @@ -11,12 +10,16 @@ ] core_modules = [ - "sv_custom_exceptions", - "sockets", - "handlers", "update_system", "main_tree_handler", - "events", "node_group", "group_handlers" + "sv_custom_exceptions", "update_system", + "sockets", "socket_data", + "handlers", + "events", "node_group", + "tasks", + "group_update_system", + "event_system", ] + def sv_register_modules(modules): for m in modules: if m.__name__ != "sverchok.menu": @@ -25,7 +28,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/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/events.py b/core/events.py index 3f413d03ee..9c82dee3b8 100644 --- a/core/events.py +++ b/core/events.py @@ -6,85 +6,91 @@ # 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 from collections.abc import Iterable -from enum import Enum, auto -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.node_tree import SverchCustomTreeNode, SverchCustomTree - SvNode = Union[SverchCustomTreeNode, SvGroupTreeNode, Node] + from sverchok.core.node_group import SvGroupTree as GrTree, \ + SvGroupTreeNode as GrNode + from sverchok.node_tree import SverchCustomTreeNode, SverchCustomTree as SvTree + SvNode = Union[SverchCustomTreeNode, GrNode, Node] class TreeEvent: - 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 - - def __init__(self, event_type: str, tree: SverchCustomTree, updated_nodes: Iterable[SvNode] = None, cancel=True): - self.type = event_type + """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): self.tree = tree - self.updated_nodes = updated_nodes - self.cancel = cancel 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 + return f"<{type(self).__name__} {self.tree.name=}>" + + +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 + + 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): + """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): + super().__init__(tree) self.updated_nodes = updated_nodes - self.to_update = group_nodes_path[-1].is_active - @property - def tree(self) -> SvGroupTree: - return self.group_node.node_tree - 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) +class GroupTreeEvent(TreeEvent): + """The same as Tree event but inside a group tree""" + tree: GrTree + update_path: list[GrNode] + + def __init__(self, tree, update_path): + super().__init__(tree) + self.update_path = 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): + super().__init__(tree, update_path) + self.updated_nodes = 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_handlers.py b/core/group_handlers.py deleted file mode 100644 index d6cdb01f49..0000000000 --- a/core/group_handlers.py +++ /dev/null @@ -1,385 +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 collections import defaultdict -from time import time -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.utils.tree_structure import Tree, 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] - -NodeId = NewType('NodeId', str) -Path = NewType('Path', str) - - -class MainHandler: - @classmethod - def update(cls, event: GroupEvent) -> 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) - - @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] - - # 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) - - 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: - NodesUpdater.add_task(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 = NodeIdManager.generate_path(group_nodes_path) - for node in group_nodes_path[-1].node_tree.nodes: - yield NodesStatuses.get(node, path).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) - for node in group_nodes_path[-1].node_tree.nodes: - yield NodesStatuses.get(node, path).update_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: - 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])\ - -> 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) - - 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) - - # 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': - 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) - 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 - - -def node_updater(node: Node, group_node: SvGroupTreeNode): - """ - 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 - node_error = None - try: - if bl_node.bl_idname in {'NodeGroupInput', 'NodeGroupOutput'}: - bl_node.process(group_node) - elif hasattr(bl_node, 'process'): - yield node # yield only normal nodes - 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 diff --git a/core/group_update_system.py b/core/group_update_system.py new file mode 100644 index 0000000000..a73720d28d --- /dev/null +++ b/core/group_update_system.py @@ -0,0 +1,224 @@ +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 = True + + # property of some node of a group tree was changed + if type(event) is ev.GroupPropertyEvent: + 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: + 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: + trees_graph.is_updated = False + + else: + was_executed = False + + return was_executed + + +class GroupUpdateTree(us.UpdateTree): + """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 + 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: + 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 + finally: + 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'] = [] + + 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]]: + """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 + 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() + 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: + """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) + 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): + """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): + sv_tree, gr_tree = item + return self._entry_nodes[sv_tree][gr_tree] + else: + return self._group_main[item] + + def _update(self): + """Calculate relationships between group trees and main trees""" + 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']]: + """Iterate over all nested node trees""" + 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 e4056dda2a..360952deae 100644 --- a/core/handlers.py +++ b/core/handlers.py @@ -3,7 +3,9 @@ from sverchok import old_nodes from sverchok import data_structure -from sverchok.core.update_system import clear_system_cache, reset_timing_graphs +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 from sverchok.utils.handle_blender_data import BlTrees @@ -82,29 +84,26 @@ 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 - # 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): """ 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 @@ -149,15 +148,10 @@ 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 - gh.NodesStatuses.reset_data() - gh.GroupContextTrees.reset_data() - import sverchok.core.main_tree_handler as mh - mh.NodesStatuses.reset_data() - mh.ContextTrees.reset_data() + handle_event(ev.FileEvent()) @persistent @@ -217,6 +211,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 8c72894d75..e69de29bb2 100644 --- a/core/main_tree_handler.py +++ b/core/main_tree_handler.py @@ -1,528 +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 - -import gc -from collections import defaultdict -from functools import partial -from time import time, perf_counter -from typing import Dict, NamedTuple, Generator, Optional, Iterator, Tuple, Union - -import bpy -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 -from sverchok.utils.tree_structure import Tree, Node -from sverchok.utils.handle_blender_data import BlTrees, BlTree -from sverchok.utils.profile import profile - - -class TreeHandler: - - @staticmethod - def send(event: TreeEvent): - - # this should be first other wise other instructions can spoil the node statistic to redraw - if NodesUpdater.is_running(): - if event.cancel: - NodesUpdater.cancel_task() - 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: - ContextTrees.mark_nodes_outdated(event.tree, event.updated_nodes) - 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 - if NodesUpdater.has_task(): - return # ignore the event - # this event was caused my update system itself and should be ignored - 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: - 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) - event.tree['FORCE_UPDATE'] = True - - # Unknown event - else: - raise TypeError(f'Detected unknown event - {event}') - - # Add update tusk for the tree - NodesUpdater.add_task(event) - - @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""" - for node in bl_tree.nodes: - yield NodesStatuses.get(node).error - - @staticmethod - def get_update_time(bl_tree) -> Iterator[Optional[float]]: - for node in bl_tree.nodes: - yield NodesStatuses.get(node).update_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) - - -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() - 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 - - _node_tree_area: Optional[bpy.types.Area] = None - _last_node: Optional[Node] = None - - _start_time: float = None - - @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 - - @classmethod - def start_task(cls): - changed_tree = cls._event.tree - if cls.is_running(): - raise RuntimeError(f'Tree "{changed_tree.name}" already is being updated') - cls._handler = global_updater(cls._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 - break - gc.disable() - - cls._start_time = time() - - @classmethod - @profile(section="UPDATE") - def run_task(cls): - try: - # handle un-cancellable events - if cls._event.type == TreeEvent.FRAME_CHANGE: - while True: - next(cls._handler) - - # handler cancellable events - else: - if cls._last_node: - cls._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) - - cls._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) - node.bl_tween.use_custom_color = True - node.bl_tween.color = (0.7, 1.000000, 0.7) - - 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) - cls.finish_task() - - @classmethod - def cancel_task(cls): - try: - cls._handler.throw(CancelError) - except (StopIteration, RuntimeError): - pass - finally: # protection from the task to be stack forever - cls.finish_task() - - @classmethod - def finish_task(cls): - try: - gc.enable() - debug(f'Global update - {int((time() - cls._start_time) * 1000)}ms') - cls._report_progress() - finally: - cls._event, cls._handler, cls._node_tree_area, cls._last_node, cls._start_time = [None] * 5 - - @classmethod - def has_task(cls) -> bool: - return cls._event is not None - - @classmethod - def is_running(cls) -> bool: - return cls._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 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 == TreeEvent.FRAME_CHANGE: - if bl_tree.sv_animate: - was_changed = yield from tree_updater(bl_tree) - - # 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) - - # 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) - - # it has sense to call this here if you press update all button or creating group tree from selected - if was_changed: - bl_tree.update_ui() # 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: - args = [bl_tree.get_update_path()] if BlTree(bl_tree).is_group_tree else [] - bl_tree.update_ui(*args) - - -def tree_updater(bl_tree) -> 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 - - 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) - - # update node with sub update system, catch statistic - start_time = perf_counter() - node_error = yield from updater - update_time = (perf_counter() - 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 - - return tree_output_changed - - -class ContextTrees: - """It keeps trees with their states""" - _trees: Dict[str, Tree] = dict() - - @classmethod - def get(cls, bl_tree): - """Return caught tree or new if the tree was not build yet""" - 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 - - 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.""" - tree = cls._trees.get(bl_tree.tree_id) - if tree: - tree.is_updated = False - - @classmethod - def mark_nodes_outdated(cls, bl_tree, bl_nodes): - """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: - tree.nodes[bl_node.name].is_updated = False - - # 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: - del cls._trees[bl_tree.tree_id] - else: - cls._trees.clear() - - @classmethod - def calc_cam_update_time(cls, bl_tree) -> 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 - 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: - link.from_node.is_input_changed = True - else: - link.to_node.is_input_changed = True - - 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 - - @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""" - - -def node_updater(node: Node, *args) -> 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 - node.bl_tween.process(*args) - 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) -> 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(is_input_changed=should_be_updated) - 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 empty_updater(node: Node = None, **kwargs): - """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 - - -@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 e1c56270f9..5a74be5e04 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, EnumProperty +from sverchok.core.event_system import handle_event from sverchok.data_structure import extend_blender_class from mathutils import Vector from sverchok.core.sockets import socket_type_names -from sverchok.core.group_handlers import MainHandler, NodeIdManager -from sverchok.core.events import GroupEvent -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, BlTree, BlTrees -from sverchok.utils.logging import catch_log_error -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,13 +33,16 @@ class SvGroupTree(SvNodeTreeCommon, bpy.types.NodeTree): bl_icon = 'NODETREE' bl_label = 'Group tree' - handler = MainHandler - # 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): @@ -115,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(GroupEvent(GroupEvent.GROUP_TREE_UPDATE, 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""" @@ -204,7 +207,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)) + handle_event(ev.GroupPropertyEvent(self, self.get_update_path(), nodes)) def parent_nodes(self) -> Iterator['SvGroupTreeNode']: """Returns all parent nodes""" @@ -214,27 +217,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, debugger nodes""" - self.handler.send(GroupEvent(GroupEvent.EDIT_GROUP_NODE, group_nodes_path)) - - 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, NodeIdManager.extract_node_id(node)) - - # update debug nodes - if BlNode(node).is_debug_node: - with catch_log_error(): - node.process() - def get_update_path(self) -> List['SvGroupTreeNode']: """ Should be called only when the tree is opened in one of tree editors @@ -295,7 +277,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)' @@ -313,6 +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.""" + handle_event(ev.TreesGraphEvent()) self.node_tree: SvGroupTree = self.group_tree # also default values should be fixed if self.node_tree: @@ -379,58 +362,50 @@ 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 + + # 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() - 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 - - def updater(self, group_nodes_path: Optional[List['SvGroupTreeNode']] = None, - is_input_changed: bool = True) -> 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) + output_node = self.active_output() + if not input_node or not output_node: + return - 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) + 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)) - 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 [])) + 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): + 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 active_input(self) -> Optional[bpy.types.Node]: # https://developer.blender.org/T82350 @@ -438,10 +413,12 @@ def active_input(self) -> Optional[bpy.types.Node]: if node.bl_idname == 'NodeGroupInput': return node - def update(self): - if 'init_tree' in self.id_data: # tree is building by a script - let it do this - return + def active_output(self) -> Optional[bpy.types.Node]: + for node in reversed(self.node_tree.nodes): + if node.bl_idname == 'NodeGroupOutput': + return node + 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): @@ -451,12 +428,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): + handle_event(ev.TreesGraphEvent()) - 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() + def sv_free(self): + handle_event(ev.TreesGraphEvent()) class PlacingNodeOperator: @@ -837,8 +813,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.update_ui(group_nodes_path) + event = ev.GroupTreeEvent(sub_tree, sub_tree.get_update_path()) + 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'} @@ -913,41 +889,22 @@ 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 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 087a755658..f6a0fb5633 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -16,26 +16,145 @@ # # ##### 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 UserDict +from itertools import chain +from typing import NewType, Optional, Literal -sentinel = object() +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 -# 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. +SockId = NewType('SockId', str) + + +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.bl_idname in {'NodeSocketVirtual', 'NodeSocketColor'}: + continue + 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) 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,140 +162,48 @@ 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[socket.socket_id] 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): + """sets socket data for socket""" + socket_data_cache[socket.socket_id] = data -def SvGetSocket(socket, other=None, deepcopy=True): +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 """ - 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(socket.socket_id) + if data is not None: + return sv_deep_copy(data) if deepcopy else data + else: raise SvNoDataError(socket) -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): """ 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] + sock_address = socket.socket_id + if sock_address in socket_data_cache: + return socket_data_cache[sock_address] 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 a922695914..57d0edfaca 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -26,9 +26,8 @@ from bpy.types import NodeTree, NodeSocket from sverchok.core.socket_conversions import ConversionPolicies -from sverchok.core.socket_data import ( - SvGetSocketInfo, SvGetSocket, SvSetSocket, SvForgetSocket, - SvNoDataError, sentinel) +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, @@ -54,8 +53,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""" @@ -321,6 +318,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 @@ -339,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 @@ -371,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)) + _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): @@ -396,11 +399,10 @@ 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): """ - 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 @@ -410,19 +412,13 @@ def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): 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 :return: data bound to the socket """ + if self.is_output: + return sv_get_socket(self, False) - 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) prop_name = self.get_prop_name() if prop_name: @@ -433,23 +429,25 @@ 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) + """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) 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, 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'): @@ -550,31 +548,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): # 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: - 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=[])) 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): @@ -653,10 +640,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/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/tasks.py b/core/tasks.py new file mode 100644 index 0000000000..a75d801b6f --- /dev/null +++ b/core/tasks.py @@ -0,0 +1,218 @@ +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: + """ + 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 + """ + _todo: set['Task'] + _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 + + 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}"' + self._report_progress(msg) + if self.current.is_exhausted: + self._next() + + self._finish() + + def cancel(self): + """Remove all tasks in the queue and abort current one""" + 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']: + """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: + self._start() + self._current = self._todo.pop() + return self._current + else: + 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 + + # 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): + """Start time of execution the whole queue of tasks""" + 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): + """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) + + +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: + """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 + + self._updater: Generator = 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() + 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: 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 + + def __eq__(self, other: 'Task'): + return self.tree.tree_id == other.tree.tree_id + + def __hash__(self): + return hash(self.tree.tree_id) + + def __repr__(self): + return f"" + + +@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 bbe1f478f5..1477df4fd8 100644 --- a/core/update_system.py +++ b/core/update_system.py @@ -1,405 +1,593 @@ -# ##### 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.socket_data import SvNoDataError -from sverchok.utils.logging import warning, error, exception +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, Iterable + +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.core.socket_data import clear_all_socket_cache -import sverchok +from sverchok.utils.logging import log_error +from sverchok.utils.tree_walk import bfs_walk -import ast +if TYPE_CHECKING: + from sverchok.node_tree import (SverchCustomTreeNode as SvNode, + SverchCustomTree as SvTree) -graphs = [] -# graph_dicts = {} +UPDATE_KEY = "US_is_updated" +ERROR_KEY = "US_error" +TIME_KEY = "US_time" -no_data_color = (1, 0.3, 0) -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 - 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. +def control_center(event): """ - 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) + 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 = 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: + 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: + 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: + tree = UpdateTree.get(event.tree) + tree.add_outdated(event.updated_nodes) + if event.tree.sv_process: + ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) + + # update the whole tree anyway + elif type(event) is ev.ForceEvent: + UpdateTree.reset_tree(event.tree) + 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: + ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree))) + + # new file opened + elif type(event) is ev.FileEvent: + UpdateTree.reset_tree() - 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() + 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] + _sock_node: dict[NodeSocket, Node] + _links: set[tuple[NodeSocket, NodeSocket]] + + def __init__(self, tree: NodeTree): + self._tree = tree + 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) + 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() + 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 + + 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 + + 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: + 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[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', 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() + + 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(_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] + + 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 _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): + """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 + reevaluation + """ + if tree.tree_id not in cls._tree_catch: + _tree = cls(tree) + else: + _tree = cls._tree_catch[tree.tree_id] + + if refresh_tree: + # update topology + if not _tree.is_updated: + old = _tree + _tree = old.copy(tree) + + # 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: 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: + next(g) + except StopIteration: + pass + + @classmethod + def main_update(cls, tree: NodeTree, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]: + """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: + walker = up_tree._walk() + # walker = up_tree._debug_color(walker) + 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: + if up_tree._tree.show_time_mode == "Cumulative": + times = up_tree._calc_cam_update_time() 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])) + times = None + update_ui(tree, times) + + @classmethod + def reset_tree(cls, tree: NodeTree = None): + """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: - 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() + cls._tree_catch.clear() + + 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. + :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_ + + 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 __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 + + 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) + + self._copy_attrs = [ + 'is_updated', + 'is_animation_updated', + 'is_scene_updated', + '_outdated_nodes', + ] + + 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: + 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']: + """Returns nodes which are scene dependent""" + 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]]: + """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 + self._outdated_nodes = set() + # walk triggered nodes and error nodes from previous updates 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) + outdated = frozenset(self._outdated_nodes) + self._outdated_nodes.clear() + + 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) + else: + node[UPDATE_KEY] = False + + 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: + 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: '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: + 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 _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" + + # 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) - 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: + 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: + """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 + + 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_val) + + if self._supress and exc_type is not None: + if issubclass(exc_type, CancelError): + return False + return issubclass(exc_type, Exception) + + +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 - 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 + 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) -def reload_sverchok(): - data_structure.RELOAD_EVENT = False - from sverchok.core import handlers - handlers.sv_post_load([]) - reset_timing_graphs() + ns.sv_set(data) -def register(): - addon_name = sverchok.__name__ - addon = bpy.context.preferences.addons.get(addon_name) - if addon: - update_error_colors(addon.preferences, []) +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/node_tree.py b/node_tree.py index 3ce419c0f2..8ea275a5c5 100644 --- a/node_tree.py +++ b/node_tree.py @@ -13,10 +13,9 @@ from bpy.props import StringProperty, BoolProperty, EnumProperty from bpy.types import NodeTree -from sverchok.core.socket_data 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.core.sv_custom_exceptions import SvNoDataError +import sverchok.core.events as ev +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 @@ -27,12 +26,22 @@ 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: """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: 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): @@ -71,6 +80,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 ''' @@ -113,19 +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(TreeEvent(TreeEvent.TREE_UPDATE, 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()) - 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(), - update=lambda s, c: s.update_ui(), - description="Mode of showing node update timings", - ) sv_show_socket_menus: BoolProperty( name = "Show socket menus", @@ -146,59 +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(TreeEvent.TREE_UPDATE, 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(TreeEvent(TreeEvent.FORCE_UPDATE, 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(TreeEvent(TreeEvent.NODES_UPDATE, self, nodes, cancel)) + 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""" - 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)) - - def process_ani(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 """ - 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())) - - def update_ui(self): - """ 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]) - for node, error, update in zip(self.nodes, nodes_errors, update_time): - if hasattr(node, 'update_ui'): - node.update_ui(error, update) + handle_event(ev.AnimationEvent(self, frame_changed, animation_playing)) class UpdateNodes: @@ -229,7 +209,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): @@ -280,18 +263,21 @@ def sv_new_input(self, socket_type, name, **attrib_dict): def free(self): """Called upon the node removal""" + # custom free function self.sv_free() - for s in self.outputs: + # free sockets memory + for s in chain(self.inputs, 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""" self.n_id = "" + for sock in chain(self.inputs, self.outputs): + sock.s_id = '' self.sv_copy(original) def update(self): @@ -304,7 +290,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 @@ -312,23 +298,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/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 5b62fdc1dc..8cc5257185 100644 --- a/nodes/color/texture_evaluate_mk2.py +++ b/nodes/color/texture_evaluate_mk2.py @@ -21,7 +21,6 @@ from mathutils import Vector from sverchok.node_tree import SverchCustomTreeNode -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) @@ -147,7 +146,7 @@ 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 sv_draw_buttons(self, context, layout): b = layout.split(factor=0.33, align=True) 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/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/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/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: diff --git a/nodes/logic/evolver.py b/nodes/logic/evolver.py index 181cc0585c..13dbe95773 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.update_system import UpdateTree 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: UpdateTree, 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, suppress=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 = 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) 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/nodes/logic/loop_out.py b/nodes/logic/loop_out.py index 3efa8bd71d..5c014fc308 100644 --- a/nodes/logic/loop_out.py +++ b/nodes/logic/loop_out.py @@ -17,20 +17,14 @@ # ##### END GPL LICENSE BLOCK ##### import bpy -from bpy.props import EnumProperty, BoolProperty +from bpy.props import EnumProperty +from sverchok.core.update_system import UpdateTree 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: - 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'} @@ -184,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") @@ -222,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 = 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) + 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 @@ -264,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, suppress=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) @@ -283,34 +281,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 = 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) + 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, suppress=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(): diff --git a/nodes/script/profile_mk3.py b/nodes/script/profile_mk3.py index 0a245d2f5c..b79b5a9568 100644 --- a/nodes/script/profile_mk3.py +++ b/nodes/script/profile_mk3.py @@ -25,7 +25,7 @@ from sverchok.node_tree import SverchCustomTreeNode 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/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 0b0da4f63a..d6514d544e 100644 --- a/nodes/transforms/texture_displace_mk2.py +++ b/nodes/transforms/texture_displace_mk2.py @@ -21,7 +21,6 @@ from mathutils import Vector, Matrix from sverchok.node_tree import SverchCustomTreeNode -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 @@ -165,7 +164,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 sv_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 ffbca537f9..5076eae0e4 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 @@ -757,7 +754,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") @@ -784,7 +781,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/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/old_nodes/texture_displace.py b/old_nodes/texture_displace.py index 5afeff41ca..c40fd90d5e 100644 --- a/old_nodes/texture_displace.py +++ b/old_nodes/texture_displace.py @@ -21,7 +21,6 @@ from mathutils import Vector, Matrix from sverchok.node_tree import SverchCustomTreeNode -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 @@ -172,7 +171,7 @@ 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 sv_draw_buttons(self, context, layout): is_vector = self.out_mode in ['RGB to XYZ', 'HSV to XYZ', 'HLS to XYZ'] diff --git a/old_nodes/texture_evaluate.py b/old_nodes/texture_evaluate.py index 2fc8a80cfe..d73c636e85 100644 --- a/old_nodes/texture_evaluate.py +++ b/old_nodes/texture_evaluate.py @@ -21,7 +21,6 @@ from mathutils import Vector from sverchok.node_tree import SverchCustomTreeNode -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,9 +143,10 @@ 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 sv_draw_buttons(self, context, layout): + self.draw_animatable_buttons(layout, icon_only=True) b = layout.split(factor=0.33, align=True) b.label(text='Mapping:') b.prop(self, 'tex_coord_type', expand=False, text='') diff --git a/settings.py b/settings.py index bcdf9731a7..e084deb73e 100644 --- a/settings.py +++ b/settings.py @@ -7,7 +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 main_tree_handler # don't remove this should fix #4229 (temp solution) +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 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/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 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) diff --git a/ui/nodeview_keymaps.py b/ui/nodeview_keymaps.py index 105ec52042..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 main_tree_handler.NodesUpdater.is_running(): - main_tree_handler.NodesUpdater.cancel_task() + if ts.tasks: + ts.tasks.cancel() return {'FINISHED'} @classmethod diff --git a/utils/geom.py b/utils/geom.py index a97f367313..a9034259d0 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -1151,7 +1151,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/nodes_mixins/recursive_nodes.py b/utils/nodes_mixins/recursive_nodes.py index 173d0fd94b..5dff422be0 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]] 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 diff --git a/utils/tree_structure.py b/utils/tree_structure.py index b527f97fb5..af56577b73 100644 --- a/utils/tree_structure.py +++ b/utils/tree_structure.py @@ -321,4 +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}"' + f'TO "{self.to_node.name}.{self.to_socket.identifier}"' \ No newline at end of file diff --git a/utils/tree_walk.py b/utils/tree_walk.py index 45764618d7..cbcc2485d9 100644 --- a/utils/tree_walk.py +++ b/utils/tree_walk.py @@ -11,7 +11,44 @@ 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, Iterator + +T = TypeVar('T') +NextFunc = Callable[[T], Iterable[T]] + + +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. + 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') + + +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):