|
|
@@ -13,12 +13,21 @@ from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
|
|
|
from .utilities.utilities import get_socket_maps, relink_socket_map, do_relink
|
|
|
from .mantis_dataclasses import MantisSocketTemplate
|
|
|
|
|
|
+MANTIS_VERSION_MAJOR=0
|
|
|
+MANTIS_VERSION_MINOR=13
|
|
|
+MANTIS_VERSION_SUB=5
|
|
|
+
|
|
|
FLOAT_EPSILON=0.0001 # used to check against floating point inaccuracy
|
|
|
links_sort_key= lambda a : (-a.multi_input_sort_id, -a.sub_sort_id)
|
|
|
|
|
|
selection_sync_node_to_object = {} # what objects a node spawns
|
|
|
selection_sync_object_to_node = {} # what node an object comes from
|
|
|
|
|
|
+# The reroute cache is used to accelerate reading reroute nodes
|
|
|
+# it uses the tree name and hash to ensure reroutes are only updated when needed
|
|
|
+# reroutes recursively find their source and remember it
|
|
|
+reroute_cache = {}
|
|
|
+
|
|
|
def TellClasses():
|
|
|
#Why use a function to do this? Because I don't need every class to register.
|
|
|
return [ MantisTree,
|
|
|
@@ -44,15 +53,13 @@ def valid_interface_types(cls : NodeTree, socket_idname : str):
|
|
|
return ['NodeSocketGeometry']
|
|
|
else: # once versioning is finished this will be unnecesary.
|
|
|
return socket_idname in tell_valid_bl_idnames()
|
|
|
-
|
|
|
def fix_reroute_colors(tree):
|
|
|
context = bpy.context
|
|
|
if any((tree.is_executing, tree.is_exporting, tree.do_live_update==False, context.space_data is None) ):
|
|
|
return
|
|
|
- from collections import deque
|
|
|
from .utilities.utilities import find_reroute_start_socket
|
|
|
from .socket_definitions import MantisSocket
|
|
|
- reroutes_without_color = deque()
|
|
|
+ reroutes_without_color = []
|
|
|
for n in tree.nodes:
|
|
|
if n.bl_idname=='NodeReroute' and n.inputs[0].bl_idname == "NodeSocketColor":
|
|
|
reroutes_without_color.append(n)
|
|
|
@@ -69,11 +76,11 @@ def fix_reroute_colors(tree):
|
|
|
|
|
|
#functions to identify the state of the system using hashes
|
|
|
# this function runs a lot so it should be optimized as well as possible.
|
|
|
-def hash_tree(tree):
|
|
|
+def hash_tree(tree, recurse=True):
|
|
|
trees=set(); links=[]; hash_data=""
|
|
|
for node in tree.nodes:
|
|
|
hash_data+=str(node.name)
|
|
|
- if hasattr(node, 'node_tree') and node.node_tree:
|
|
|
+ if recurse and hasattr(node, 'node_tree') and node.node_tree:
|
|
|
trees.add(node.node_tree)
|
|
|
for other_tree in trees:
|
|
|
hash_data+=str(hash_tree(other_tree))
|
|
|
@@ -84,9 +91,157 @@ def hash_tree(tree):
|
|
|
links.sort(); hash_data+=''.join(links)
|
|
|
return hash(hash_data)
|
|
|
|
|
|
-MANTIS_VERSION_MAJOR=0
|
|
|
-MANTIS_VERSION_MINOR=13
|
|
|
-MANTIS_VERSION_SUB=5
|
|
|
+# TODO: I should see if I can make these work without every using NodeSocket.links
|
|
|
+# as that getter is very slow at scale
|
|
|
+# however with the cache in place it is probably negligible
|
|
|
+def update_reroute_cache_item_source_recursively(reroute, tree_cache, source):
|
|
|
+ # find all the inputs to this reroute and update their destination sets
|
|
|
+ # every reroute ancestor pushes to this destination... but not every recursive-child
|
|
|
+ # of the root reroute necessarily does, so pushing back like this is right
|
|
|
+ # since each reroute node only has one ancestor, this is easy
|
|
|
+ from .mantis_dataclasses import RerouteCacheItem
|
|
|
+ tree= reroute.id_data
|
|
|
+ reroutes = {reroute}
|
|
|
+ while(reroutes):
|
|
|
+ reroute = reroutes.pop()
|
|
|
+ # for making a new cache item
|
|
|
+ if (cache_object := tree_cache.get(reroute.name)) is None:
|
|
|
+ cache_object = RerouteCacheItem(
|
|
|
+ reroute_name=reroute.name,
|
|
|
+ reroute_tree_name=tree.name,
|
|
|
+ )
|
|
|
+ tree_cache[reroute.name] = cache_object
|
|
|
+ cache_object.source = source
|
|
|
+ if reroute.outputs[0].is_linked: # reroutes are always single-output
|
|
|
+ for l in reroute.outputs[0].links: # with potentially many links
|
|
|
+ if l.to_node.bl_idname == 'NodeReroute':
|
|
|
+ reroutes.add(l.to_node)
|
|
|
+
|
|
|
+def update_reroute_cache_item_destination_recursively(reroute, tree_cache, destination):
|
|
|
+ # find all the inputs to this reroute and update their destination sets
|
|
|
+ # every reroute ancestor pushes to this destination... but not every recursive-child
|
|
|
+ # of the root reroute necessarily does, so pushing back like this is right
|
|
|
+ # since each reroute node only has one ancestor, this is easy
|
|
|
+ from .mantis_dataclasses import RerouteCacheItem
|
|
|
+ tree= reroute.id_data
|
|
|
+ while(reroute):
|
|
|
+ # for making a new cache item
|
|
|
+ if (cache_object := tree_cache.get(reroute.name)) is None:
|
|
|
+ cache_object = RerouteCacheItem(
|
|
|
+ reroute_name=reroute.name,
|
|
|
+ reroute_tree_name=tree.name,
|
|
|
+ destinations= {destination},
|
|
|
+ )
|
|
|
+ tree_cache[reroute.name] = cache_object
|
|
|
+ else:
|
|
|
+ cache_object.destinations.add(destination)
|
|
|
+ # Odd: if I check reroute.inputs[0].is_linked with live updates, CRASH.
|
|
|
+ if rr_links := reroute.inputs[0].links:
|
|
|
+ # reroutes are always single-input with at most 1 link
|
|
|
+ if (l := rr_links[0]).from_node == 'NodeReroute':
|
|
|
+ reroute = l.from_node
|
|
|
+ continue
|
|
|
+ reroute = None
|
|
|
+
|
|
|
+# Update the reroute cache from the link
|
|
|
+# NOTE this is updated as the user adds links by the node.insert_link() callback
|
|
|
+def reroute_cache_handle_link(tree, tree_cache, link):
|
|
|
+ from .mantis_dataclasses import RerouteCacheItem
|
|
|
+ # if it is a link to or from a reroute, add or update the cache item
|
|
|
+ # first check if the cache item is stale
|
|
|
+ # then try and fill the source and destination - make sure to recursively fill them
|
|
|
+ # if it isn't connected to a source or destination, it's part of a network
|
|
|
+ # We only need to check the ends, and we can do both the destination and source
|
|
|
+ # passes simultaneously since they don't depend on each other.
|
|
|
+ # NOTE that we use socket index to make things explicit and fast
|
|
|
+ if link.from_node.bl_idname == 'NodeReroute' and link.to_node.bl_idname != 'NodeReroute':
|
|
|
+ other_node = link.to_node
|
|
|
+ other_socket = link.to_socket
|
|
|
+ for socket_index, check_socket in enumerate(other_node.inputs):
|
|
|
+ if check_socket.identifier == other_socket.identifier: break
|
|
|
+ else:
|
|
|
+ raise RuntimeError("Error, cannot make reroute cache for link: "
|
|
|
+ f"{link.from_node.name}:{link.from_socket.name} ==>"
|
|
|
+ f"{link.to_node.name}:{link.to_socket.name}")
|
|
|
+ reroute = link.from_node
|
|
|
+ destination = (other_node.name, socket_index)
|
|
|
+ if (cache_object := tree_cache.get(reroute.name)) is None:
|
|
|
+ cache_object = RerouteCacheItem(
|
|
|
+ reroute_name=reroute.name,
|
|
|
+ reroute_tree_name=tree.name,
|
|
|
+ destinations= {destination},
|
|
|
+ )
|
|
|
+ tree_cache[reroute.name]=cache_object
|
|
|
+ update_reroute_cache_item_destination_recursively(reroute, tree_cache,
|
|
|
+ destination=destination)
|
|
|
+ elif link.from_node.bl_idname != 'NodeReroute' and link.to_node.bl_idname == 'NodeReroute':
|
|
|
+ other_node = link.from_node
|
|
|
+ other_socket = link.from_socket
|
|
|
+ for socket_index, check_socket in enumerate(other_node.outputs):
|
|
|
+ if check_socket.identifier == other_socket.identifier: break
|
|
|
+ else:
|
|
|
+ raise RuntimeError("Error, cannot make reroute cache for link: "
|
|
|
+ f"{link.from_node.name}:{link.from_socket.name} ==>"
|
|
|
+ f"{link.to_node.name}:{link.to_socket.name}")
|
|
|
+ reroute = link.to_node
|
|
|
+ source = (link.from_node.name, socket_index)
|
|
|
+ if (cache_object := tree_cache.get(reroute.name)) is None:
|
|
|
+ cache_object = RerouteCacheItem(
|
|
|
+ reroute_name=reroute.name,
|
|
|
+ reroute_tree_name=tree.name,
|
|
|
+ source = source,
|
|
|
+ )
|
|
|
+ tree_cache[reroute.name]=cache_object
|
|
|
+ update_reroute_cache_item_source_recursively(reroute, tree_cache,
|
|
|
+ source=source)
|
|
|
+
|
|
|
+def build_reroute_cache(tree, links):
|
|
|
+ # we'll track this to know if a tree update should invalidate the hash on incremental update
|
|
|
+ tree.number_of_reroute_links = 0
|
|
|
+ # make sure to clear the previous cache or confusing things will happen
|
|
|
+ tree_cache = {}; reroute_cache[tree.name] = tree_cache
|
|
|
+ tree_cache['_MANTIS_RESERVED_TREE_HASH'] = tree.hash
|
|
|
+ for link in links:
|
|
|
+ if link.from_node.bl_idname == 'NodeReroute' or link.to_node.bl_idname == 'NodeReroute':
|
|
|
+ tree.number_of_reroute_links += 1
|
|
|
+ reroute_cache_handle_link(tree, tree_cache, link)
|
|
|
+
|
|
|
+def tree_update_common(tree): # set the reroute colors
|
|
|
+ if tree.is_quitting: return
|
|
|
+ if (bpy.app.version == (4,4,0)): # CTD bug in that version
|
|
|
+ return
|
|
|
+ fix_reroute_colors(tree)
|
|
|
+ # intialize it if e.g the tree is new.
|
|
|
+ if (tree_cache := reroute_cache.get(tree.name)) is None:
|
|
|
+ tree_cache = {}; reroute_cache[tree.name] = tree_cache
|
|
|
+ # events fire in this order:
|
|
|
+ # - node.insert_link (one for each node)
|
|
|
+ # - node.update (one for each node)
|
|
|
+ # - tree.update
|
|
|
+ # so if the node's insert_link handled things, we don't need to check
|
|
|
+ tree.hash = str(hash_tree(tree, recurse=False))
|
|
|
+ if tree.reroute_link_handled: # we can update the hash, it's still good.
|
|
|
+ if tree_cache := reroute_cache.get(tree.name):
|
|
|
+ tree_cache["_MANTIS_RESERVED_TREE_HASH"]=tree.hash
|
|
|
+ tree.reroute_link_handled = False # always make sure to reset the flag
|
|
|
+ else:
|
|
|
+ reroute_links = []
|
|
|
+ # set the UI tree hash for e.g. reroute cache
|
|
|
+ for link in tree.links:
|
|
|
+ if link.from_node.bl_idname == 'NodeReroute' or link.to_node.bl_idname == 'NodeReroute':
|
|
|
+ reroute_links.append(link)
|
|
|
+ if len(reroute_links) == tree.number_of_reroute_links:
|
|
|
+ if tree_cache := reroute_cache.get(tree.name): # we can update the hash, it's still good.
|
|
|
+ tree_cache["_MANTIS_RESERVED_TREE_HASH"] = tree.hash
|
|
|
+ else: # there is no cache after all. How did that happen?
|
|
|
+ build_reroute_cache(tree, reroute_links)
|
|
|
+ # NOTE I put a lot of effort into catching deleted links but it just isn't practical
|
|
|
+ # to catch this and it is almost free to rebuild the cache.
|
|
|
+ else:
|
|
|
+ build_reroute_cache(tree, reroute_links)
|
|
|
+ # reset the count but do not update the hash.
|
|
|
+ tree.number_of_reroute_links = len(reroute_links)
|
|
|
+
|
|
|
|
|
|
class MantisTree(NodeTree):
|
|
|
'''A custom node tree type that will show up in the editor type list'''
|
|
|
@@ -95,7 +250,8 @@ class MantisTree(NodeTree):
|
|
|
bl_icon = 'OUTLINER_OB_ARMATURE'
|
|
|
|
|
|
tree_valid:BoolProperty(default=False)
|
|
|
- hash:StringProperty(default='')
|
|
|
+ hash:StringProperty(default='') # use for UI updates, reroute cache
|
|
|
+ hash_recursive:StringProperty(default='') # use for checking Mantis state
|
|
|
do_live_update:BoolProperty(default=True) # use this to disable updates for e.g. scripts
|
|
|
# operator settings for re-exporting the tree.
|
|
|
filepath:StringProperty(default="", subtype='FILE_PATH')
|
|
|
@@ -103,6 +259,7 @@ class MantisTree(NodeTree):
|
|
|
#
|
|
|
is_executing:BoolProperty(default=False)
|
|
|
is_exporting:BoolProperty(default=False)
|
|
|
+ is_quitting:BoolProperty(default=False)
|
|
|
execution_id:StringProperty(default='')
|
|
|
# prev_execution_id:StringProperty(default='')
|
|
|
mantis_version:IntVectorProperty(default=[
|
|
|
@@ -114,6 +271,11 @@ class MantisTree(NodeTree):
|
|
|
#added to work around a bug in 4.5.0 LTS
|
|
|
interface_helper : StringProperty(default='')
|
|
|
|
|
|
+ # for reroute cache, to check if it is still good
|
|
|
+ number_of_reroute_links : IntProperty(default=0)
|
|
|
+ # flag to indicate a link was already handled this update
|
|
|
+ reroute_link_handled : BoolProperty(default=False)
|
|
|
+
|
|
|
parsed_tree={}
|
|
|
|
|
|
# NOTE that these are class properties that should be serialized
|
|
|
@@ -129,16 +291,19 @@ class MantisTree(NodeTree):
|
|
|
def valid_socket_type(cls : NodeTree, socket_idname: str):
|
|
|
return valid_interface_types(cls, socket_idname)
|
|
|
|
|
|
- def update(self): # set the reroute colors
|
|
|
- if (bpy.app.version >= (4,4,0)):
|
|
|
- fix_reroute_colors(self)
|
|
|
+ def update(self):
|
|
|
+ tree_update_common(self)
|
|
|
|
|
|
def update_tree(self, context = None, force=False, error_popups=False):
|
|
|
if self.is_exporting:
|
|
|
return
|
|
|
my_hash = str( hash_tree(self) )
|
|
|
- if (my_hash != self.hash) or force:
|
|
|
- self.hash = my_hash
|
|
|
+ my_ui_hash = str( hash_tree(self, recurse=False))
|
|
|
+ tree_cache = reroute_cache.get(self.name)
|
|
|
+ if tree_cache['_MANTIS_RESERVED_TREE_HASH'] != my_ui_hash:
|
|
|
+ build_reroute_cache(self, self.links) # somehow it became invalid, rebuild it.
|
|
|
+ if (my_hash != self.hash_recursive) or force:
|
|
|
+ self.hash_recursive = my_hash
|
|
|
self.is_executing = True
|
|
|
from .execution import readtree
|
|
|
prGreen("Validating Tree: %s" % self.name)
|
|
|
@@ -154,11 +319,15 @@ class MantisTree(NodeTree):
|
|
|
except Exception as e:
|
|
|
prRed("Failed to update node tree due to error.")
|
|
|
self.tree_valid = False
|
|
|
- self.hash='' # unset the hash to mark the tree as un-parsed.
|
|
|
+ self.hash_recursive='' # unset the hash to mark the tree as un-parsed.
|
|
|
raise e
|
|
|
finally:
|
|
|
scene.render.use_lock_interface = False
|
|
|
self.is_executing = False
|
|
|
+ if self.parsed_tree is None:
|
|
|
+ self.parsed_tree = {}
|
|
|
+ return 1 # failure
|
|
|
+ return 0 # success
|
|
|
|
|
|
def display_update(self, context):
|
|
|
if self.is_exporting:
|
|
|
@@ -181,7 +350,7 @@ class MantisTree(NodeTree):
|
|
|
|
|
|
def execute_tree(self,context, error_popups = False, profile=False):
|
|
|
self.prevent_next_exec = False
|
|
|
- if not self.hash:
|
|
|
+ if not self.hash_recursive:
|
|
|
return
|
|
|
if self.is_exporting or self.is_executing:
|
|
|
return
|
|
|
@@ -207,20 +376,25 @@ class SchemaTree(NodeTree):
|
|
|
do_live_update:BoolProperty(default=True) # default to true so that updates work
|
|
|
is_executing:BoolProperty(default=False)
|
|
|
is_exporting:BoolProperty(default=False)
|
|
|
+ is_quitting:BoolProperty(default=False)
|
|
|
|
|
|
mantis_version:IntVectorProperty(default=[
|
|
|
MANTIS_VERSION_MAJOR, MANTIS_VERSION_MINOR, MANTIS_VERSION_SUB])
|
|
|
# see the note in MantisTree
|
|
|
interface_helper : StringProperty(default='')
|
|
|
|
|
|
+ # for reroute cache, to check if it is still good
|
|
|
+ number_of_reroute_links : IntProperty(default=0)
|
|
|
+ # flag to indicate a link was already handled this update
|
|
|
+ reroute_link_handled : BoolProperty(default=False)
|
|
|
+
|
|
|
if (bpy.app.version < (4, 4, 0) or bpy.app.version >= (4,5,0)): # in 4.4 this leads to a crash
|
|
|
@classmethod
|
|
|
def valid_socket_type(cls : NodeTree, socket_idname: str):
|
|
|
return valid_interface_types(cls, socket_idname)
|
|
|
|
|
|
- def update(self): # set the reroute colors
|
|
|
- if (bpy.app.version >= (4,4,0)):
|
|
|
- fix_reroute_colors(self)
|
|
|
+ def update(self):
|
|
|
+ tree_update_common(self)
|
|
|
|
|
|
|
|
|
#TODO: do a better job explaining how MantisNode and MantisUINode relate.
|
|
|
@@ -286,6 +460,18 @@ class MantisUINode:
|
|
|
if template.use_multi_input: # this is an array
|
|
|
socket.display_shape = 'SQUARE_DOT'
|
|
|
|
|
|
+ def insert_link(self, link):
|
|
|
+ if (bpy.app.version == (4, 4, 0)):
|
|
|
+ return # this causes a crash due to a bug.
|
|
|
+ tree = self.id_data
|
|
|
+ if tree.reroute_link_handled: return
|
|
|
+ if ( link.from_node.bl_idname == 'NodeReroute' or
|
|
|
+ link.to_node.bl_idname == 'NodeReroute' ):
|
|
|
+ tree.number_of_reroute_links+=1
|
|
|
+ reroute_cache_handle_link(tree=tree,
|
|
|
+ tree_cache=reroute_cache.get(tree.name), link=link)
|
|
|
+ tree.reroute_link_handled = True
|
|
|
+
|
|
|
# NOTE
|
|
|
# MantisUINode has no proper mantis libraryclass on its own, so:
|
|
|
# register()'s are in SUBclasses; they have to run AFTER library is defined
|
|
|
@@ -676,12 +862,12 @@ custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "Utili
|
|
|
"xFormArmature", "xFormGeometryObject", "xFormObjectInstance", "xFormCurvePin",]
|
|
|
|
|
|
# filters for determining if a link is a hierarchy link or a non-hierarchy (cyclic) link.
|
|
|
-from_name_filter = ["Driver",]
|
|
|
-to_name_filter = [
|
|
|
+from_name_filter = frozenset({"Driver",})
|
|
|
+to_name_filter = frozenset({
|
|
|
"Custom Object xForm Override",
|
|
|
"Custom Object",
|
|
|
"Deform Bones",
|
|
|
- ]
|
|
|
+ })
|
|
|
|
|
|
# nodes that must be solved as if they were Schema because they send arrays out.
|
|
|
array_output_types = [
|
|
|
@@ -723,22 +909,42 @@ class MantisExecutionContext():
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
+# using these frozen sets accelerate lookup when checking for this
|
|
|
+FILL_PARAMETERS_SKIP_ROOTS = frozenset({ 'MANTIS_AUTOGENERATED', 'SCHEMA_AUTOGENERATED', })
|
|
|
+FILL_PARAMETERS_SKIP_NAMES = frozenset({ "NodeGroupOutput", "NodeGroupInput", })
|
|
|
+FILL_PARAMETERS_SKIP_SOCKETS = frozenset({ 'RelationshipSocket', 'xFormSocket', })
|
|
|
+from .socket_definitions import no_default_value
|
|
|
+
|
|
|
class MantisNode:
|
|
|
"""
|
|
|
This class contains the basic interface for a Mantis Node.
|
|
|
A MantisNode is used internally by Mantis to represent the final evaluated node graph.
|
|
|
It gets generated with data from a MantisUINode when the graph is read.
|
|
|
"""
|
|
|
+ __slots__ = (
|
|
|
+ "base_tree", "signature", "ui_signature",
|
|
|
+ "inputs", "outputs",
|
|
|
+ "parameters", "drivers", "bObject",
|
|
|
+ "hierarchy_connections", "connections",
|
|
|
+ "hierarchy_dependencies", "dependencies",
|
|
|
+ "unmet_hierarchy_dependency_count",
|
|
|
+ "prepared", "executed", "execution_prepared",
|
|
|
+ "mContext",
|
|
|
+ "is_schema_prototype"
|
|
|
+ )
|
|
|
+ node_type = 'UNINITIALIZED'
|
|
|
+ socket_templates = None
|
|
|
+ property_socket_map = {}
|
|
|
def __init__(self, signature : tuple,
|
|
|
base_tree : bpy.types.NodeTree,
|
|
|
- socket_templates : list[MantisSocketTemplate]=[],):
|
|
|
+ socket_templates : list[MantisSocketTemplate]=[],
|
|
|
+ additional_parameters :dict = {}, ):
|
|
|
self.base_tree=base_tree
|
|
|
self.signature = signature
|
|
|
self.ui_signature = signature
|
|
|
self.inputs = MantisNodeSocketCollection(node=self, is_input=True)
|
|
|
self.outputs = MantisNodeSocketCollection(node=self, is_input=False)
|
|
|
- self.parameters, self.drivers = {}, {}; self.bObject=None
|
|
|
- self.node_type='UNINITIALIZED'
|
|
|
+ self.parameters, self.drivers = {}, []; self.bObject=None
|
|
|
self.hierarchy_connections, self.connections = [], []
|
|
|
self.hierarchy_dependencies, self.dependencies = [], []
|
|
|
self.unmet_hierarchy_dependency_count = float("inf") # we will set this when filling the dependencies lists
|
|
|
@@ -746,14 +952,14 @@ class MantisNode:
|
|
|
self.execution_prepared = False
|
|
|
# execution_prepared is for tracking prep state in execution, so that I can avoid preparing nodes
|
|
|
# again without changing the readtree code much.
|
|
|
- self.socket_templates = socket_templates
|
|
|
self.mContext = None # for now I am gonna set this at runtime
|
|
|
+ self.is_schema_prototype = False
|
|
|
# I know it isn't "beautiful OOP" or whatever, but it is just easier
|
|
|
# code should be simple and do things in the simplest way.
|
|
|
# in the future I can refactor it, but it will require changes to 100+
|
|
|
# classes, instead of adding about 5 lines of code elsewhere.
|
|
|
if self.socket_templates:
|
|
|
- self.init_sockets()
|
|
|
+ self.init_sockets( additional_parameters )
|
|
|
|
|
|
@property
|
|
|
def name(self):
|
|
|
@@ -766,13 +972,40 @@ class MantisNode:
|
|
|
|
|
|
def reset_execution(self) -> None:
|
|
|
""" Reset the node for additional execution without re-building the tree."""
|
|
|
- self.drivers={}; self.bObject=None
|
|
|
+ self.drivers.clear(); self.bObject=None
|
|
|
self.executed = False
|
|
|
self.execution_prepared = False
|
|
|
|
|
|
- def init_sockets(self) -> None:
|
|
|
- self.inputs.init_sockets(self.socket_templates)
|
|
|
- self.outputs.init_sockets(self.socket_templates)
|
|
|
+ def init_sockets(self, additional_parameters : dict = {}) -> None:
|
|
|
+ # Formerly I was doing this:
|
|
|
+ # self.inputs.init_sockets(self.socket_templates)
|
|
|
+ # self.outputs.init_sockets(self.socket_templates)
|
|
|
+ # Since doing each one causes some redundancy, we'll just reimplement it
|
|
|
+ # This is a highly used and therefore expensive function so we have to make small
|
|
|
+ # optimizations like this, even tho it is ugly.
|
|
|
+ traverse_in, traverse_out = {}, {}
|
|
|
+ for socket in self.socket_templates:
|
|
|
+ if socket.traverse_target:
|
|
|
+ if socket.is_input:
|
|
|
+ traverse_in[socket.name] = socket.traverse_target
|
|
|
+ else:
|
|
|
+ traverse_out[socket.name] = socket.traverse_target
|
|
|
+ if socket.ui_only == True: continue # used in UI but not in generation
|
|
|
+ if socket.is_input:
|
|
|
+ self.inputs[socket.name] = NodeSocket(is_input=True, name=socket.name,
|
|
|
+ node=self,)
|
|
|
+ else:
|
|
|
+ self.outputs[socket.name] = NodeSocket(is_input=False, name=socket.name,
|
|
|
+ node=self,)
|
|
|
+ self.parameters[socket.name] = None
|
|
|
+ for traverse_owner, traverse_target in traverse_in.items():
|
|
|
+ self.inputs[traverse_owner].traverse_target = self.outputs[traverse_target]
|
|
|
+ self.inputs[traverse_owner].can_traverse = True
|
|
|
+ for traverse_owner, traverse_target in traverse_out.items():
|
|
|
+ self.outputs[traverse_owner].traverse_target = self.inputs[traverse_target]
|
|
|
+ self.outputs[traverse_owner].can_traverse = True
|
|
|
+ for key, value in additional_parameters.items():
|
|
|
+ self.parameters[key]=value
|
|
|
|
|
|
def init_parameters(self, additional_parameters = {}) -> None:
|
|
|
for socket in self.inputs:
|
|
|
@@ -782,36 +1015,67 @@ class MantisNode:
|
|
|
for key, value in additional_parameters.items():
|
|
|
self.parameters[key]=value
|
|
|
|
|
|
- def gen_property_socket_map(self, prop_owner_group=None) -> dict:
|
|
|
+ # We really only need to do this once at class registration
|
|
|
+ # Therefore the goal is not to have any mutable data here
|
|
|
+ # ======================= READ BELOW =======================
|
|
|
+ # We have the following classes of Mantis Socket ==> Blender Proeprty relationships:
|
|
|
+ # Scalar: 1 Mantis scalar ==> 1 Blender property. 1 Filler.
|
|
|
+ # Vector: 1 Mantis Vector socket ==> Blender vector property. 1 Filler.
|
|
|
+ # Scalar Combo: 1 Mantis scalar ==> many Blender scalars. 1 Filler per property.
|
|
|
+ # Vector Combo: 1 Mantis vector ==> vector of Blender vector property. 1 Filler per property.
|
|
|
+ # In every case, 1 filler per Blender property.
|
|
|
+ @classmethod
|
|
|
+ def gen_property_socket_map(cls, prop_owner_group=None) -> dict:
|
|
|
from .mantis_dataclasses import MantisSocketFiller
|
|
|
from dataclasses import replace
|
|
|
from bpy.app import version as bpy_version
|
|
|
+ if cls.property_socket_map: # retrieve the cache if possible
|
|
|
+ return cls.property_socket_map
|
|
|
props_sockets = {}
|
|
|
- for s_template in self.socket_templates:
|
|
|
+ for s_template in cls.socket_templates:
|
|
|
if s_template.min_bpy_version:
|
|
|
if bpy_version < s_template.min_bpy_version:
|
|
|
prPurple(f"Skipping socket {s_template.name} because "
|
|
|
f"it requires Blender {s_template.min_bpy_version} or higher")
|
|
|
continue # We can't set or drive this obviously.
|
|
|
- if prop_owner_group is not None and \
|
|
|
- s_template.prop_owner_group != prop_owner_group:
|
|
|
+ if prop_owner_group is not None and s_template.prop_owner_group != prop_owner_group:
|
|
|
continue # for composite objects like bones
|
|
|
- if not s_template.blender_property:
|
|
|
- continue
|
|
|
- filler = MantisSocketFiller(socket_name=s_template.name)
|
|
|
- if isinstance(s_template.blender_property, str):
|
|
|
- filler = replace(filler)
|
|
|
- filler.property_name = s_template.blender_property
|
|
|
- filler.default_value=s_template.default_value
|
|
|
- props_sockets[s_template.blender_property]=filler
|
|
|
- elif isinstance(s_template.blender_property, (tuple, list)):
|
|
|
- for index, sub_prop in enumerate(s_template.blender_property):
|
|
|
- filler = replace(filler)
|
|
|
- filler.property_name = sub_prop
|
|
|
- filler.value_index=index
|
|
|
- filler.default_value = s_template.default_value[index]
|
|
|
- filler.combo_property=True
|
|
|
- props_sockets[sub_prop]= filler
|
|
|
+ if not s_template.blender_property: # this one has to be handled by the node itself.
|
|
|
+ continue # Or maybe it is UI-only, in any case we don't need to do anything here.
|
|
|
+ # Default to (Scalar <==> Scalar), as tuples for compatibility with other code paths
|
|
|
+ vector_property, combo = False, False # TODO: make socket templates pre-tuple-ize this
|
|
|
+ defaults = ( s_template.default_value, ); properties = ( s_template.blender_property, )
|
|
|
+ if isinstance( s_template.default_value, (list, tuple) ):
|
|
|
+ vector_property = True # this tells us if MANTIS provides a vector
|
|
|
+ # if Blender consumes a vector, then we map the vector to the vector. Easy.
|
|
|
+ if isinstance(s_template.blender_property, (tuple, list)):
|
|
|
+ # But maybe we provide data from one Mantis Socket to many Blender properties...
|
|
|
+ combo=True; properties = s_template.blender_property
|
|
|
+ if vector_property: # we have to break the default value up so we can map
|
|
|
+ # mantis data element-wise to a vector of Blender properties
|
|
|
+ defaults = s_template.default_value
|
|
|
+ # Now iteratre through the properties and make 1 filler per.
|
|
|
+ for i, property in enumerate(properties):
|
|
|
+ default_index = i if vector_property and combo else 0 # only needed for Combo Vector
|
|
|
+ default = defaults[default_index] # usually 1:1 except for Combo Scalar
|
|
|
+ key = s_template.name + f"_{default_index}" + (f"_{property}" if combo else "")
|
|
|
+ value_index = default_index if combo and vector_property else None
|
|
|
+ from .utilities import socket_filler_operators as operators
|
|
|
+ property_value_operator = None
|
|
|
+ if s_template.property_value_operator:
|
|
|
+ property_value_operator = getattr(operators, s_template.property_value_operator)
|
|
|
+ props_sockets[key] = MantisSocketFiller(
|
|
|
+ socket_name = s_template.name, # This is a dict because node
|
|
|
+ property_name = property, # instances should be able
|
|
|
+ value_index = value_index, # to edit Fillers.
|
|
|
+ default_value = default, # The keys are not inherently
|
|
|
+ combo_property = combo, # meaningful and are
|
|
|
+ vector_property = vector_property, # NOT USED DOWNSTREAM!
|
|
|
+ is_animatable = s_template.is_animatable,
|
|
|
+ property_group = s_template.prop_owner_group,
|
|
|
+ property_value_operator = property_value_operator)
|
|
|
+ if prop_owner_group is None:
|
|
|
+ cls.property_socket_map = props_sockets # We should cache this one!
|
|
|
return props_sockets
|
|
|
|
|
|
def set_traverse(self, traversal_pairs = [(str, str)]) -> None:
|
|
|
@@ -912,40 +1176,36 @@ class MantisNode:
|
|
|
return prop
|
|
|
|
|
|
def fill_parameters(self, ui_node=None) -> None:
|
|
|
- from .utilities.utilities import get_ui_node
|
|
|
- from .utilities.node_common import get_socket_value
|
|
|
+ from mathutils import Matrix
|
|
|
+ from .utilities.utilities import handle_overlay_detect
|
|
|
if not ui_node:
|
|
|
- if ( (self.signature[0] in ["MANTIS_AUTOGENERATED", "SCHEMA_AUTOGENERATED" ]) or
|
|
|
- (self.signature[-1] in ["NodeGroupOutput", "NodeGroupInput"]) ): # I think this is harmless
|
|
|
+ if ( (self.signature[0] in FILL_PARAMETERS_SKIP_ROOTS) or
|
|
|
+ (self.signature[-1] in FILL_PARAMETERS_SKIP_NAMES) ): # I think this is harmless
|
|
|
return None
|
|
|
- else: # BUG shouldn't this use ui_signature??
|
|
|
- ui_node = get_ui_node(self.signature, self.base_tree)
|
|
|
- if not ui_node:
|
|
|
- raise RuntimeError(wrapRed("No UI Node found for... %s" % ( [self.base_tree] + list(self.signature[1:]) ) ) )
|
|
|
+ raise RuntimeError(wrapRed("No UI Node found for... %s" % ( [self.base_tree] + list(self.signature[1:]) ) ) )
|
|
|
+ ui_inputs = ui_node.inputs; ui_outputs = ui_node.outputs
|
|
|
for key in self.parameters.keys():
|
|
|
- node_socket = ui_node.inputs.get(key)
|
|
|
if self.parameters[key] is not None: # the parameters are usually initialized as None.
|
|
|
continue # will be filled by the node itself
|
|
|
- if not node_socket: #maybe the node socket has no name
|
|
|
- if ( ( len(ui_node.inputs) == 0) and ( len(ui_node.outputs) == 1) ):
|
|
|
- node_socket = ui_node.outputs[0] # this is a simple input node.
|
|
|
- elif key == 'Name': # for Links we just use the Node Label, or if there is no label, the name.
|
|
|
- self.parameters[key] = ui_node.label if ui_node.label else ui_node.name
|
|
|
- continue
|
|
|
- if node_socket:
|
|
|
- if node_socket.bl_idname in ['RelationshipSocket', 'xFormSocket']: continue
|
|
|
+ if node_socket := ui_inputs.get(key):
|
|
|
+ if node_socket.bl_idname in FILL_PARAMETERS_SKIP_SOCKETS: continue
|
|
|
elif node_socket.is_linked and (not node_socket.is_output): continue
|
|
|
# we will get the value from the link, because this is a linked input port.
|
|
|
# very importantly, we do not pass linked outputs
|
|
|
# fill these because they are probably Input nodes.
|
|
|
- elif hasattr(node_socket, "default_value"):
|
|
|
- if (value := get_socket_value(node_socket)) is not None:
|
|
|
- if isinstance(value, str) and "$" in value:
|
|
|
- from .utilities.utilities import handle_overlay_detect
|
|
|
- value = handle_overlay_detect(self, node_socket.identifier, value)
|
|
|
- self.parameters[key] = value
|
|
|
- else:
|
|
|
- raise RuntimeError(wrapRed("No value found for " + self.__repr__() + " when filling out node parameters for " + ui_node.name + "::"+node_socket.name))
|
|
|
+ if node_socket.bl_idname in no_default_value: continue
|
|
|
+ if node_socket.bl_idname == 'MatrixSocket':
|
|
|
+ v = node_socket.default_value
|
|
|
+ self.parameters[key] = Matrix(( tuple(v[0:4]),
|
|
|
+ tuple(v[4:8]),
|
|
|
+ tuple(v[8:12]),
|
|
|
+ tuple(v[12:16]) ))
|
|
|
+ elif (value := node_socket.default_value) is not None:
|
|
|
+ if isinstance(value, str) and "$" in value:
|
|
|
+ value = handle_overlay_detect(self, node_socket.identifier, value)
|
|
|
+ self.parameters[key] = value
|
|
|
+ else:
|
|
|
+ raise RuntimeError(wrapRed("No value found for " + self.__repr__() + " when filling out node parameters for " + ui_node.name + "::"+node_socket.name))
|
|
|
|
|
|
# I don't think this works! but I like the idea
|
|
|
def call_on_all_ancestors(self, *args, **kwargs):
|
|
|
@@ -1045,6 +1305,10 @@ class MantisNode:
|
|
|
|
|
|
# do I need this and the link class above?
|
|
|
class DummyLink:
|
|
|
+ __slots__ = ("from_socket", "to_socket",
|
|
|
+ "from_mantis_node", "to_mantis_node",
|
|
|
+ "multi_input_sort_id",
|
|
|
+ "original_from")
|
|
|
#gonna use this for faking links to keep the interface consistent
|
|
|
def __init__(self, from_socket, to_socket, from_mantis_node=None, to_mantis_node=None, original_from=None, multi_input_sort_id=0):
|
|
|
self.from_socket = from_socket
|
|
|
@@ -1065,7 +1329,7 @@ class DummyLink:
|
|
|
def detect_hierarchy_link(from_node, from_socket, to_node, to_socket,):
|
|
|
if to_node.node_type in ['DUMMY_SCHEMA', 'SCHEMA']:
|
|
|
return False #TODO: find out if filtering SCHEMA types is wise
|
|
|
- if from_socket in from_name_filter and to_socket in ["Array"]:
|
|
|
+ if from_socket in from_name_filter and to_socket in {"Array"}:
|
|
|
# SPECIAL CASE: arrays essentially extend the non-hierarchy edge
|
|
|
# so if the connection to the array isn't hierarchy, the array node
|
|
|
# in the middle gets disconnected and never executes.
|
|
|
@@ -1080,10 +1344,16 @@ def detect_hierarchy_link(from_node, from_socket, to_node, to_socket,):
|
|
|
return True
|
|
|
|
|
|
class NodeLink:
|
|
|
- from_node = None
|
|
|
- from_socket = None
|
|
|
- to_node = None
|
|
|
- to_socket = None
|
|
|
+ __slots__ = (
|
|
|
+ "from_node",
|
|
|
+ "from_socket",
|
|
|
+ "to_node",
|
|
|
+ "to_socket",
|
|
|
+ "multi_input_sort_id",
|
|
|
+ "sub_sort_id",
|
|
|
+ "is_hierarchy",
|
|
|
+ "is_alive",
|
|
|
+ )
|
|
|
|
|
|
def __init__(self, from_node, from_socket, to_node, to_socket, multi_input_sort_id=0, sub_sort_id=0):
|
|
|
if from_node.signature == to_node.signature:
|
|
|
@@ -1127,6 +1397,15 @@ class NodeLink:
|
|
|
init_dependencies(to_node)
|
|
|
|
|
|
class NodeSocket:
|
|
|
+ __slots__ = (
|
|
|
+ "can_traverse",
|
|
|
+ "traverse_target",
|
|
|
+ "node",
|
|
|
+ "name",
|
|
|
+ "is_input",
|
|
|
+ "links",
|
|
|
+ "is_linked",
|
|
|
+ )
|
|
|
def __init__(self, is_input = False,
|
|
|
node = None, name = None,
|
|
|
traverse_target = None):
|
|
|
@@ -1180,21 +1459,16 @@ class NodeSocket:
|
|
|
return self.node.__repr__() + "::" + self.name
|
|
|
|
|
|
class MantisNodeSocketCollection(dict):
|
|
|
+ __slots__ = ("is_input", "node",)
|
|
|
def __init__(self, node, is_input=False):
|
|
|
self.is_input = is_input
|
|
|
self.node = node
|
|
|
|
|
|
def init_sockets(self, sockets):
|
|
|
for socket in sockets:
|
|
|
- if isinstance(socket, str):
|
|
|
- self[socket] = NodeSocket(is_input=self.is_input, name=socket, node=self.node)
|
|
|
- elif isinstance(socket, MantisSocketTemplate):
|
|
|
- if socket.ui_only == True: continue # used in UI but not in generation
|
|
|
- if socket.is_input != self.is_input: continue
|
|
|
- self[socket.name] = NodeSocket(is_input=self.is_input, name=socket.name, node=self.node)
|
|
|
- else:
|
|
|
- raise RuntimeError(f"NodeSocketCollection keys must be str or MantisSocketTemplate, not {type(socket)}")
|
|
|
-
|
|
|
+ if socket.ui_only == True: continue # used in UI but not in generation
|
|
|
+ if socket.is_input != self.is_input: continue
|
|
|
+ self[socket.name] = NodeSocket(is_input=self.is_input, name=socket.name, node=self.node)
|
|
|
def __delitem__(self, key):
|
|
|
"""Deletes a node socket by name, and all its links."""
|
|
|
socket = self[key]
|