base_definitions.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. #Mantis Nodes Base
  2. import bpy
  3. from bpy.props import (BoolProperty, StringProperty, EnumProperty, CollectionProperty, \
  4. IntProperty, IntVectorProperty, PointerProperty, BoolVectorProperty)
  5. from . import ops_nodegroup
  6. from bpy.types import NodeTree, Node, PropertyGroup, Operator, UIList, Panel
  7. from .utilities import (prRed, prGreen, prPurple, prWhite,
  8. prOrange,
  9. wrapRed, wrapGreen, wrapPurple, wrapWhite,
  10. wrapOrange,)
  11. from .utilities import get_socket_maps, relink_socket_map, do_relink
  12. def TellClasses():
  13. #Why use a function to do this? Because I don't need every class to register.
  14. return [ MantisTree,
  15. SchemaTree,
  16. MantisNodeGroup,
  17. SchemaGroup,
  18. ]
  19. def error_popup_draw(self, context):
  20. self.layout.label(text="Error executing tree. See Console.")
  21. class MantisTree(NodeTree):
  22. '''A custom node tree type that will show up in the editor type list'''
  23. bl_idname = 'MantisTree'
  24. bl_label = "Rigging Nodes"
  25. bl_icon = 'OUTLINER_OB_ARMATURE'
  26. tree_valid:BoolProperty(default=False)
  27. do_live_update:BoolProperty(default=True) # use this to disable updates for e.g. scripts
  28. num_links:IntProperty(default=-1)
  29. filepath:StringProperty(default="", subtype='FILE_PATH')
  30. is_executing:BoolProperty(default=False)
  31. is_exporting:BoolProperty(default=False)
  32. execution_id:StringProperty(default='')
  33. mantis_version:IntVectorProperty(default=[0,9,2])
  34. # this prevents the node group from executing on the next depsgraph update
  35. # because I don't always have control over when the dg update happens.
  36. prevent_next_exec:BoolProperty(default=False)
  37. parsed_tree={}
  38. if bpy.app.version >= (3, 2): # in 3.1 this can lead to a crash
  39. @classmethod
  40. def valid_socket_type(cls, socket_type: str):
  41. # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
  42. from .socket_definitions import Tell_bl_idnames
  43. return socket_type in Tell_bl_idnames()
  44. # thank you, Sverchok
  45. def update_tree(self, context = None):
  46. if self.is_exporting:
  47. return
  48. # return
  49. self.is_executing = True
  50. from . import readtree
  51. prGreen("Validating Tree: %s" % self.name)
  52. try:
  53. self.parsed_tree = readtree.parse_tree(self)
  54. if context:
  55. self.display_update(context)
  56. self.is_executing = False
  57. self.tree_valid = True
  58. except GraphError as e:
  59. prRed("Failed to update node tree due to error.")
  60. self.tree_valid = False
  61. self.is_executing = False
  62. raise e
  63. finally:
  64. self.is_executing = False
  65. def display_update(self, context):
  66. if self.is_exporting:
  67. return
  68. self.is_executing = True
  69. current_tree = bpy.context.space_data.path[-1].node_tree
  70. for node in current_tree.nodes:
  71. if hasattr(node, "display_update"):
  72. try:
  73. node.display_update(self.parsed_tree, context)
  74. except Exception as e:
  75. print("Node \"%s\" failed to update display with error: %s" %(wrapGreen(node.name), wrapRed(e)))
  76. self.is_executing = False
  77. # TODO: deal with invalid links properly.
  78. # - Non-hierarchy links should be ignored in the circle-check and so the links should be marked valid in such a circle
  79. # - hierarchy-links should be marked invalid and prevent the tree from executing.
  80. def execute_tree(self,context, error_popups = False):
  81. self.prevent_next_exec = False
  82. if self.is_exporting:
  83. return
  84. # return
  85. prGreen("Executing Tree: %s" % self.name)
  86. self.is_executing = True
  87. from . import readtree
  88. try:
  89. readtree.execute_tree(self.parsed_tree, self, context, error_popups)
  90. except RecursionError as e:
  91. prRed("Recursion error while parsing tree.")
  92. finally:
  93. self.is_executing = False
  94. class SchemaTree(NodeTree):
  95. '''A node tree representing a schema to generate a Mantis tree'''
  96. bl_idname = 'SchemaTree'
  97. bl_label = "Rigging Nodes Schema"
  98. bl_icon = 'RIGID_BODY_CONSTRAINT'
  99. # these are only needed for consistent interface, but should not be used
  100. do_live_update:BoolProperty(default=True) # default to true so that updates work
  101. is_executing:BoolProperty(default=False)
  102. is_exporting:BoolProperty(default=False)
  103. mantis_version:IntVectorProperty(default=[0,9,2])
  104. if bpy.app.version >= (3, 2): # in 3.1 this can lead to a crash
  105. @classmethod
  106. def valid_socket_type(cls, socket_type: str):
  107. # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
  108. from .socket_definitions import Tell_bl_idnames
  109. return socket_type in Tell_bl_idnames()
  110. # thank you, Sverchok
  111. #TODO: do a better job explaining how the following two classes relate.
  112. class MantisNode:
  113. """
  114. This class contains the basic interface for a Mantis Node.
  115. A MantisNode is used internally by Mantis to represent the final evaluated node graph.
  116. It gets generated with data from a MantisUINode when the graph is read.
  117. """
  118. def flush_links(self):
  119. for inp in self.inputs.values():
  120. inp.flush_links()
  121. for out in self.outputs.values():
  122. out.flush_links()
  123. def evaluate_input(self, input_name, index=0):
  124. from .node_container_common import trace_single_line
  125. if not (self.inputs.get(input_name)): # get the named parameter if there is no input
  126. if input_name == 'Meta-Armature':
  127. prOrange("beans are not frogs")
  128. return self.parameters.get(input_name) # this will return None if the parameter does not exist.
  129. # this trace() should give a key error if there is a problem
  130. # it is NOT handled here because it should NOT happen - so I want the error message.
  131. trace = trace_single_line(self, input_name, index)
  132. prop = trace[0][-1].parameters[trace[1].name] #trace[0] = the list of traced nodes; read its parameters
  133. return prop
  134. def fill_parameters(self, ui_node=None):
  135. from .utilities import get_node_prototype
  136. from .node_container_common import get_socket_value
  137. if not ui_node:
  138. if ( (self.signature[0] in ["MANTIS_AUTOGENERATED", "SCHEMA_AUTOGENERATED" ]) or
  139. (self.signature[-1] in ["NodeGroupOutput", "NodeGroupInput"]) ): # I think this is harmless
  140. return None
  141. else:
  142. ui_node = get_node_prototype(self.signature, self.base_tree)
  143. if not ui_node:
  144. raise RuntimeError(wrapRed("No node prototype found for... %s" % ( [self.base_tree] + list(self.signature[1:]) ) ) )
  145. for key in self.parameters.keys():
  146. node_socket = ui_node.inputs.get(key)
  147. if self.parameters[key] is not None: # the parameters are usually initialized as None.
  148. continue # will be filled by the node itself
  149. if not node_socket: #maybe the node socket has no name
  150. if ( ( len(ui_node.inputs) == 0) and ( len(ui_node.outputs) == 1) ):
  151. # this is a simple input node.
  152. node_socket = ui_node.outputs[0]
  153. elif key == 'Name': # for Links we just use the Node Label, or if there is no label, the name.
  154. self.parameters[key] = ui_node.label if ui_node.label else ui_node.name
  155. continue
  156. else:
  157. pass
  158. if node_socket:
  159. if node_socket.bl_idname in ['RelationshipSocket', 'xFormSocket']:
  160. continue
  161. elif node_socket.is_linked and (not node_socket.is_output):
  162. pass # we will get the value from the link, because this is a linked input port.
  163. # very importantly, we do not pass linked outputs- fill these because they are probably Input nodes.
  164. elif hasattr(node_socket, "default_value"):
  165. if (value := get_socket_value(node_socket)) is not None:
  166. self.parameters[key] = value
  167. # TODO: try and remove the input if it is not needed (for performance speed)
  168. else:
  169. raise RuntimeError(wrapRed("No value found for " + self.__repr__() + " when filling out node parameters for " + ui_node.name + "::"+node_socket.name))
  170. else:
  171. pass
  172. def bPrepare(self, bContext=None):
  173. return
  174. def bExecute(self, bContext=None):
  175. return
  176. def bFinalize(self, bContext=None):
  177. return
  178. def __repr__(self):
  179. return self.signature.__repr__()
  180. class MantisUINode:
  181. """
  182. This class contains the common user-interface features of Mantis nodes.
  183. MantisUINode objects will spawn one or more MantisNode objects when the graph is evaluated.
  184. The MantisNode objects will pull the data from the UI node and use it to generate the graph.
  185. """
  186. @classmethod
  187. def poll(cls, ntree):
  188. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  189. def insert_link(self, link):
  190. context = bpy.context
  191. if context.space_data:
  192. node_tree = context.space_data.path[0].node_tree
  193. from . import readtree
  194. if node_tree.do_live_update:
  195. node_tree.update_tree(context)
  196. if (link.to_socket.is_linked == False):
  197. node_tree.num_links+=1
  198. elif (link.to_socket.is_multi_input):
  199. node_tree.num_links+=1
  200. class SchemaUINode:
  201. @classmethod
  202. def poll(cls, ntree):
  203. return (ntree.bl_idname in ['SchemaTree'])
  204. class LinkNode(MantisUINode):
  205. @classmethod
  206. def poll(cls, ntree):
  207. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  208. class xFormNode(MantisUINode):
  209. @classmethod
  210. def poll(cls, ntree):
  211. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  212. class DeformerNode(MantisUINode):
  213. @classmethod
  214. def poll(cls, ntree):
  215. return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
  216. def poll_node_tree(self, object):
  217. if isinstance(object, MantisTree):
  218. return True
  219. return False
  220. # TODO: try to check identifiers instead of name.
  221. def node_group_update(node, force = False):
  222. if not force:
  223. if (node.id_data.do_live_update == False) or (node.id_data.is_executing == True):
  224. return
  225. # note: if (node.id_data.is_exporting == True) I need to be able to update so I can make links.
  226. toggle_update = node.id_data.do_live_update
  227. node.id_data.do_live_update = False
  228. identifiers_in={socket.identifier:socket for socket in node.inputs}
  229. identifiers_out={socket.identifier:socket for socket in node.outputs}
  230. if node.node_tree is None:
  231. node.inputs.clear(); node.outputs.clear()
  232. node.id_data.do_live_update = toggle_update
  233. return
  234. found_in, found_out = [], []
  235. update_input, update_output = False, False
  236. for item in node.node_tree.interface.items_tree:
  237. if item.item_type != "SOCKET": continue
  238. if item.in_out == 'OUTPUT':
  239. if s:= identifiers_out.get(item.identifier): # if the requested output doesn't exist, update
  240. found_out.append(item.identifier)
  241. if update_output: continue
  242. if s.bl_idname != item.socket_type: update_output = True; continue
  243. else: update_output = True; continue
  244. else:
  245. if s:= identifiers_in.get(item.identifier): # if the requested input doesn't exist, update
  246. found_in.append(item.identifier)
  247. if update_input: continue # done here
  248. if s.bl_idname != item.socket_type: update_input = True; continue
  249. else: update_input = True; continue
  250. # Schema has an extra input for Length and for Extend.
  251. if node.bl_idname == 'MantisSchemaGroup':
  252. found_in.extend(['Schema Length', ''])
  253. # if we have too many elements, just get rid of the ones we don't need
  254. if len(node.inputs) > len(found_in):#
  255. for inp in node.inputs:
  256. if inp.identifier in found_in: continue
  257. node.inputs.remove(inp)
  258. if len(node.outputs) > len(found_out):
  259. for out in node.outputs:
  260. if out.identifier in found_out: continue
  261. node.outputs.remove(out)
  262. #
  263. if len(node.inputs) > 0 and (inp := node.inputs[-1]).bl_idname == 'WildcardSocket' and inp.is_linked:
  264. update_input = True
  265. if len(node.outputs) > 0 and (out := node.outputs[-1]).bl_idname == 'WildcardSocket' and out.is_linked:
  266. update_output = True
  267. #
  268. if not (update_input or update_output):
  269. node.id_data.do_live_update = toggle_update
  270. return
  271. if update_input or update_output:
  272. socket_map_in, socket_map_out = get_socket_maps(node)
  273. if update_input :
  274. if node.bl_idname == 'MantisSchemaGroup':
  275. schema_length=0
  276. if sl := node.inputs.get("Schema Length"):
  277. schema_length = sl.default_value
  278. # sometimes this isn't available yet # TODO not happy about this solution
  279. node.inputs.clear()
  280. if node.bl_idname == 'MantisSchemaGroup':
  281. node.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  282. node.inputs['Schema Length'].default_value = schema_length
  283. if update_output: node.outputs.clear()
  284. for item in node.node_tree.interface.items_tree:
  285. if item.item_type != "SOCKET": continue
  286. if (item.in_out == 'INPUT' and update_input):
  287. relink_socket_map(node, node.inputs, socket_map_in, item)
  288. if (item.in_out == 'OUTPUT' and update_output):
  289. relink_socket_map(node, node.outputs, socket_map_out, item)
  290. # at this point there is no wildcard socket
  291. if '__extend__' in socket_map_in.keys():
  292. do_relink(node, None, socket_map_in, in_out='INPUT', parent_name='Constant' )
  293. node.id_data.do_live_update = toggle_update
  294. def node_tree_prop_update(self, context):
  295. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  296. return # so we check if an update is currently running.
  297. self.is_updating = True
  298. node_group_update(self)
  299. self.is_updating = False
  300. if self.bl_idname in ['MantisSchemaGroup'] and self.node_tree is not None:
  301. if len(self.inputs) == 0:
  302. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  303. if self.inputs[-1].bl_idname != "WildcardSocket":
  304. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  305. from bpy.types import NodeCustomGroup
  306. class MantisNodeGroup(Node, MantisUINode):
  307. bl_idname = "MantisNodeGroup"
  308. bl_label = "Node Group"
  309. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree, update=node_tree_prop_update,)
  310. is_updating:BoolProperty(default=False)
  311. def update(self):
  312. live_update = self.id_data.do_live_update
  313. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  314. return # so we check if an update is currently running.
  315. try:
  316. self.is_updating = True
  317. node_group_update(self)
  318. self.is_updating = False
  319. finally: # we need to reset this regardless of whether or not the operation succeeds!
  320. self.is_updating = False
  321. self.id_data.do_live_update = live_update # ensure this remains the same
  322. def draw_buttons(self, context, layout):
  323. row = layout.row(align=True)
  324. row.prop(self, "node_tree", text="")
  325. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  326. class GraphError(Exception):
  327. pass
  328. def get_signature_from_edited_tree(node, context):
  329. sig_path=[None,]
  330. for item in context.space_data.path[:-1]:
  331. sig_path.append(item.node_tree.nodes.active.name)
  332. return tuple(sig_path+[node.name])
  333. def poll_node_tree_schema(self, object):
  334. if isinstance(object, SchemaTree):
  335. return True
  336. return False
  337. # TODO tiny UI problem - inserting new links into the tree will not place them in the right place.
  338. class SchemaGroup(Node, MantisUINode):
  339. bl_idname = "MantisSchemaGroup"
  340. bl_label = "Node Schema"
  341. node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree_schema, update=node_tree_prop_update,)
  342. is_updating:BoolProperty(default=False)
  343. def draw_buttons(self, context, layout):
  344. row = layout.row(align=True)
  345. row.prop(self, "node_tree", text="")
  346. row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
  347. def update(self):
  348. live_update = self.id_data.do_live_update
  349. if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
  350. return # so we check if an update is currently running.
  351. self.is_updating = True
  352. try:
  353. node_group_update(self)
  354. # reset things if necessary:
  355. if self.node_tree:
  356. if len(self.inputs) == 0:
  357. self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
  358. if self.inputs[-1].bl_idname != "WildcardSocket":
  359. self.inputs.new("WildcardSocket", "", identifier="__extend__")
  360. finally: # we need to reset this regardless of whether or not the operation succeeds!
  361. self.is_updating = False
  362. self.id_data.do_live_update = live_update # ensure this remains the same
  363. NODES_REMOVED=["xFormRootNode"]
  364. # Node bl_idname, # Socket Name
  365. SOCKETS_REMOVED=[("UtilityDriverVariable", "Transform Channel"),
  366. ("xFormRootNode","World Out"),
  367. ("UtilitySwitch","xForm"),
  368. ("LinkDrivenParameter", "Enable")]
  369. # Node Class #Prior bl_idname # prior name # new bl_idname # new name, # Multi
  370. SOCKETS_RENAMED=[ ("LinkDrivenParameter", "DriverSocket", "Driver", "FloatSocket", "Value", False)]
  371. # NODE CLASS NAME IN_OUT SOCKET TYPE SOCKET NAME INDEX MULTI DEFAULT
  372. SOCKETS_ADDED=[("DeformerMorphTargetDeform", 'INPUT', 'BooleanSocket', "Use Shape Key", 1, False, False),
  373. ("DeformerMorphTargetDeform", 'INPUT', 'BooleanSocket', "Use Offset", 2, False, True),
  374. ("UtilityFCurve", 'INPUT', "eFCrvExtrapolationMode", "Extrapolation Mode", 0, False, 'CONSTANT')]
  375. # replace names with bl_idnames for reading the tree and solving schemas.
  376. replace_types = ["NodeGroupInput", "NodeGroupOutput", "SchemaIncomingConnection",
  377. "SchemaArrayInput", "SchemaConstInput", "SchemaConstOutput", "SchemaIndex",
  378. "SchemaOutgoingConnection", "SchemaConstantOutput", "SchemaArrayOutput",
  379. "SchemaArrayInputGet",]
  380. # anything that gets properties added in the graph... this is a clumsy approach but I need to watch for this
  381. # in schema generation and this is the easiest way to do it for now.
  382. custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "UtilityDriver", "xFormBone"]
  383. # filters for determining if a link is a hierarchy link or a non-hierarchy (cyclic) link.
  384. from_name_filter = ["Driver",]
  385. to_name_filter = [
  386. "Custom Object xForm Override",
  387. "Custom Object",
  388. "Deform Bones",
  389. ]