ops_nodegroup.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. import bpy
  2. from bpy.types import Operator
  3. from mathutils import Vector
  4. from .utilities import (prRed, prGreen, prPurple, prWhite,
  5. prOrange,
  6. wrapRed, wrapGreen, wrapPurple, wrapWhite,
  7. wrapOrange,)
  8. def TellClasses():
  9. return [
  10. MantisGroupNodes,
  11. MantisEditGroup,
  12. ExecuteNodeTree,
  13. # CreateMetaGroup,
  14. QueryNodeSockets,
  15. ForceDisplayUpdate,
  16. CleanUpNodeGraph,
  17. MantisMuteNode,
  18. # xForm
  19. AddCustomProperty,
  20. EditCustomProperty,
  21. RemoveCustomProperty,
  22. # EditFCurveNode,
  23. FcurveAddKeyframeInput,
  24. FcurveRemoveKeyframeInput,
  25. # Driver
  26. DriverAddDriverVariableInput,
  27. DriverRemoveDriverVariableInput,
  28. # Armature Link Node
  29. LinkArmatureAddTargetInput,
  30. LinkArmatureRemoveTargetInput,]
  31. # ExportNodeTreeToJSON,]
  32. def mantis_tree_poll_op(context):
  33. space = context.space_data
  34. if hasattr(space, "node_tree"):
  35. if (space.node_tree):
  36. return (space.tree_type in ["MantisTree", "SchemaTree"])
  37. return False
  38. #########################################################################3
  39. class MantisGroupNodes(Operator):
  40. """Create node-group from selected nodes"""
  41. bl_idname = "mantis.group_nodes"
  42. bl_label = "Group Nodes"
  43. @classmethod
  44. def poll(cls, context):
  45. return mantis_tree_poll_op(context)
  46. def execute(self, context):
  47. base_tree=context.space_data.path[-1].node_tree
  48. base_tree.is_exporting = True
  49. from .i_o import export_to_json, do_import
  50. from random import random
  51. grp_name = "".join([chr(int(random()*30)+35) for i in range(20)])
  52. trees=[base_tree]
  53. selected_nodes=export_to_json(trees, write_file=False, only_selected=True)
  54. selected_nodes[base_tree.name][0]["name"]=grp_name
  55. do_import(selected_nodes, context)
  56. affected_links_in = []
  57. affected_links_out = []
  58. for l in base_tree.links:
  59. if l.from_node.select and not l.to_node.select: affected_links_out.append(l)
  60. if not l.from_node.select and l.to_node.select: affected_links_in.append(l)
  61. delete_me = []
  62. all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
  63. for n in base_tree.nodes:
  64. if n.select:
  65. if n.location.x < all_nodes_bounding_box[0].x:
  66. all_nodes_bounding_box[0].x = n.location.x
  67. if n.location.y < all_nodes_bounding_box[0].y:
  68. all_nodes_bounding_box[0].y = n.location.y
  69. #
  70. if n.location.x > all_nodes_bounding_box[1].x:
  71. all_nodes_bounding_box[1].x = n.location.x
  72. if n.location.y > all_nodes_bounding_box[1].y:
  73. all_nodes_bounding_box[1].y = n.location.y
  74. delete_me.append(n)
  75. grp_node = base_tree.nodes.new('MantisNodeGroup')
  76. grp_node.node_tree = bpy.data.node_groups[grp_name]
  77. bb_center = all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1],0.5)
  78. for n in grp_node.node_tree.nodes:
  79. n.location -= bb_center
  80. grp_node.location = Vector((all_nodes_bounding_box[0].x+200, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))
  81. for n in selected_nodes[base_tree.name][2].values():
  82. for s in n["sockets"].values():
  83. if source := s.get("source"):
  84. prGreen (s["name"], source[0], source[1])
  85. base_tree_node=base_tree.nodes.get(source[0])
  86. if s["is_output"]:
  87. for output in base_tree_node.outputs:
  88. if output.identifier == source[1]:
  89. break
  90. else:
  91. raise RuntimeError(wrapRed("Socket not found when grouping"))
  92. base_tree.links.new(input=output, output=grp_node.inputs[s["name"]])
  93. else:
  94. for s_input in base_tree_node.inputs:
  95. if s_input.identifier == source[1]:
  96. break
  97. else:
  98. raise RuntimeError(wrapRed("Socket not found when grouping"))
  99. base_tree.links.new(input=grp_node.outputs[s["name"]], output=s_input)
  100. for n in delete_me: base_tree.nodes.remove(n)
  101. base_tree.nodes.active = grp_node
  102. base_tree.is_exporting = False
  103. grp_node.node_tree.name = "Group_Node.000"
  104. return {'FINISHED'}
  105. class MantisEditGroup(Operator):
  106. """Edit the group referenced by the active node (or exit the current node-group)"""
  107. bl_idname = "mantis.edit_group"
  108. bl_label = "Edit Group"
  109. @classmethod
  110. def poll(cls, context):
  111. return (
  112. mantis_tree_poll_op(context)
  113. )
  114. def execute(self, context):
  115. space = context.space_data
  116. path = space.path
  117. node = path[len(path)-1].node_tree.nodes.active
  118. if hasattr(node, "node_tree"):
  119. if (node.node_tree):
  120. path.append(node.node_tree, node=node)
  121. path[0].node_tree.display_update(context)
  122. return {"FINISHED"}
  123. elif len(path) > 1:
  124. path.pop()
  125. path[0].node_tree.display_update(context)
  126. # get the active node in the current path
  127. path[len(path)-1].node_tree.nodes.active.update() # call update to force the node group to check if its tree has changed
  128. return {"CANCELLED"}
  129. class ExecuteNodeTree(Operator):
  130. """Execute this node tree"""
  131. bl_idname = "mantis.execute_node_tree"
  132. bl_label = "Execute Node Tree"
  133. @classmethod
  134. def poll(cls, context):
  135. return (mantis_tree_poll_op(context))
  136. def execute(self, context):
  137. from time import time
  138. from .utilities import wrapGreen
  139. tree=context.space_data.path[0].node_tree
  140. import cProfile
  141. from os import environ
  142. start_time = time()
  143. do_profile=False
  144. print (environ.get("DOPROFILE"))
  145. if environ.get("DOPROFILE"):
  146. do_profile=True
  147. if do_profile:
  148. import pstats, io
  149. from pstats import SortKey
  150. with cProfile.Profile() as pr:
  151. tree.update_tree(context)
  152. tree.execute_tree(context)
  153. # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
  154. s = io.StringIO()
  155. sortby = SortKey.TIME
  156. # sortby = SortKey.CUMULATIVE
  157. ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
  158. ps.print_stats(20) # print the top 20
  159. print(s.getvalue())
  160. else:
  161. tree.update_tree(context)
  162. tree.execute_tree(context)
  163. prGreen("Finished executing tree in %f seconds" % (time() - start_time))
  164. return {"FINISHED"}
  165. class QueryNodeSockets(Operator):
  166. """Utility Operator for querying the data in a socket"""
  167. bl_idname = "mantis.query_sockets"
  168. bl_label = "Query Node Sockets"
  169. @classmethod
  170. def poll(cls, context):
  171. return (mantis_tree_poll_op(context))
  172. def execute(self, context):
  173. node = context.active_node
  174. print ("Node type: ", node.bl_idname)
  175. # This is useful. Todo: reimplement this eventually.
  176. return {"FINISHED"}
  177. class ForceDisplayUpdate(Operator):
  178. """Utility Operator for querying the data in a socket"""
  179. bl_idname = "mantis.force_display_update"
  180. bl_label = "Force Mantis Display Update"
  181. @classmethod
  182. def poll(cls, context):
  183. return (mantis_tree_poll_op(context))
  184. def execute(self, context):
  185. base_tree = bpy.context.space_data.path[0].node_tree
  186. base_tree.display_update(context)
  187. return {"FINISHED"}
  188. class CleanUpNodeGraph(bpy.types.Operator):
  189. """Clean Up Node Graph"""
  190. bl_idname = "mantis.nodes_cleanup"
  191. bl_label = "Clean Up Node Graph"
  192. bl_options = {'REGISTER', 'UNDO'}
  193. # num_iterations=bpy.props.IntProperty(default=8)
  194. @classmethod
  195. def poll(cls, context):
  196. return hasattr(context, 'active_node')
  197. def execute(self, context):
  198. base_tree=context.space_data.path[-1].node_tree
  199. from .utilities import SugiyamaGraph
  200. SugiyamaGraph(base_tree, 12)
  201. return {'FINISHED'}
  202. class MantisMuteNode(Operator):
  203. """Mantis Test Operator"""
  204. bl_idname = "mantis.mute_node"
  205. bl_label = "Mute Node"
  206. @classmethod
  207. def poll(cls, context):
  208. return (mantis_tree_poll_op(context))
  209. def execute(self, context):
  210. path = context.space_data.path
  211. node = path[len(path)-1].node_tree.nodes.active
  212. node.mute = not node.mute
  213. # There should only be one of these
  214. if (enable := node.inputs.get("Enable")):
  215. # annoyingly, 'mute' and 'enable' are opposites
  216. enable.default_value = not node.mute
  217. if (hide := node.inputs.get("Hide")):
  218. hide.default_value = node.mute
  219. return {"FINISHED"}
  220. ePropertyType =(
  221. ('BOOL' , "Boolean", "Boolean", 0),
  222. ('INT' , "Integer", "Integer", 1),
  223. ('FLOAT' , "Float" , "Float" , 2),
  224. ('VECTOR', "Vector" , "Vector" , 3),
  225. ('STRING', "String" , "String" , 4),
  226. #('ENUM' , "Enum" , "Enum" , 5),
  227. )
  228. from .base_definitions import xFormNode
  229. class AddCustomProperty(bpy.types.Operator):
  230. """Add Custom Property to xForm Node"""
  231. bl_idname = "mantis.add_custom_property"
  232. bl_label = "Add Custom Property"
  233. prop_type : bpy.props.EnumProperty(
  234. items=ePropertyType,
  235. name="New Property Type",
  236. description="Type of data for new Property",
  237. default = 'BOOL',)
  238. prop_name : bpy.props.StringProperty(default='Prop')
  239. min:bpy.props.FloatProperty(default = 0)
  240. max:bpy.props.FloatProperty(default = 1)
  241. soft_min:bpy.props.FloatProperty(default = 0)
  242. soft_max:bpy.props.FloatProperty(default = 1)
  243. description:bpy.props.StringProperty(default = "")
  244. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  245. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  246. @classmethod
  247. def poll(cls, context):
  248. return True #( hasattr(context, 'node') )
  249. def invoke(self, context, event):
  250. self.tree_invoked = context.node.id_data.name
  251. self.node_invoked = context.node.name
  252. wm = context.window_manager
  253. return wm.invoke_props_dialog(self)
  254. def execute(self, context):
  255. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  256. # For whatever reason, context.node doesn't exist anymore
  257. # (probably because I use a window to execute)
  258. # so as a sort of dumb workaround I am saving it to a hidden
  259. # property of the operator... it works but Blender complains.
  260. socktype = ''
  261. if not (self.prop_name):
  262. self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")
  263. return {'CANCELLED'}
  264. if self.prop_type == 'BOOL':
  265. socktype = 'ParameterBoolSocket'
  266. if self.prop_type == 'INT':
  267. socktype = 'ParameterIntSocket'
  268. if self.prop_type == 'FLOAT':
  269. socktype = 'ParameterFloatSocket'
  270. if self.prop_type == 'VECTOR':
  271. socktype = 'ParameterVectorSocket'
  272. if self.prop_type == 'STRING':
  273. socktype = 'ParameterStringSocket'
  274. #if self.prop_type == 'ENUM':
  275. # sock_type = 'ParameterStringSocket'
  276. if (s := n.inputs.get(self.prop_name)):
  277. try:
  278. number = int(self.prop_name[-3:])
  279. # see if it has a number
  280. number+=1
  281. self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
  282. except ValueError:
  283. self.prop_name+='.001'
  284. # WRONG # HACK # TODO # BUG #
  285. new_prop = n.inputs.new( socktype, self.prop_name)
  286. if self.prop_type in ['INT','FLOAT']:
  287. new_prop.min = self.min
  288. new_prop.max = self.max
  289. new_prop.soft_min = self.soft_min
  290. new_prop.soft_max = self.soft_max
  291. new_prop.description = self.description
  292. # now do the output
  293. n.outputs.new( socktype, self.prop_name)
  294. return {'FINISHED'}
  295. def main_get_existing_custom_properties(operator, context):
  296. ret = []; i = -1
  297. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  298. for inp in n.inputs:
  299. if 'Parameter' in inp.bl_idname:
  300. ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )
  301. return ret
  302. class EditCustomProperty(bpy.types.Operator):
  303. """Edit Custom Property"""
  304. bl_idname = "mantis.edit_custom_property"
  305. bl_label = "Edit Custom Property"
  306. def get_existing_custom_properties(self, context):
  307. return main_get_existing_custom_properties(self, context)
  308. prop_edit : bpy.props.EnumProperty(
  309. items=get_existing_custom_properties,
  310. name="Property to Edit?",
  311. description="Select which property to edit",)
  312. prop_type : bpy.props.EnumProperty(
  313. items=ePropertyType,
  314. name="New Property Type",
  315. description="Type of data for new Property",
  316. default = 'BOOL',)
  317. prop_name : bpy.props.StringProperty(default='Prop')
  318. min:bpy.props.FloatProperty(default = 0)
  319. max:bpy.props.FloatProperty(default = 1)
  320. soft_min:bpy.props.FloatProperty(default = 0)
  321. soft_max:bpy.props.FloatProperty(default = 1)
  322. description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
  323. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  324. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  325. @classmethod
  326. def poll(cls, context):
  327. return True #( hasattr(context, 'node') )
  328. def invoke(self, context, event):
  329. self.tree_invoked = context.node.id_data.name
  330. self.node_invoked = context.node.name
  331. wm = context.window_manager
  332. return wm.invoke_props_dialog(self)
  333. def execute(self, context):
  334. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  335. prop = n.inputs.get( self.prop_edit )
  336. if prop:
  337. prop.name = self.prop_name
  338. if (s := n.inputs.get(self.prop_edit)):
  339. if self.prop_type in ['INT','FLOAT']:
  340. prop.min = self.min
  341. prop.max = self.max
  342. prop.soft_min = self.soft_min
  343. prop.soft_max = self.soft_max
  344. prop.description = self.description
  345. return {'FINISHED'}
  346. else:
  347. self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")
  348. class RemoveCustomProperty(bpy.types.Operator):
  349. """Remove a Custom Property from an xForm Node"""
  350. bl_idname = "mantis.remove_custom_property"
  351. bl_label = "Remove Custom Property"
  352. def get_existing_custom_properties(self, context):
  353. return main_get_existing_custom_properties(self, context)
  354. prop_remove : bpy.props.EnumProperty(
  355. items=get_existing_custom_properties,
  356. name="Property to remove?",
  357. description="Select which property to remove",)
  358. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  359. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  360. @classmethod
  361. def poll(cls, context):
  362. return True #(hasattr(context, 'active_node') )
  363. def invoke(self, context, event):
  364. self.tree_invoked = context.node.id_data.name
  365. self.node_invoked = context.node.name
  366. t = context.node.id_data
  367. # HACK the props dialog makes this necesary
  368. # because context.node only exists during the event that
  369. # was created by clicking on the node.
  370. t.nodes.active = context.node # HACK
  371. context.node.select = True # HACK
  372. # I need this bc of the callback for the enum property.
  373. # for whatever reason I can't use tree_invoked there
  374. wm = context.window_manager
  375. return wm.invoke_props_dialog(self)
  376. def execute(self, context):
  377. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  378. # For whatever reason, context.node doesn't exist anymore
  379. # (probably because I use a window to execute)
  380. # so as a sort of dumb workaround I am saving it to a hidden
  381. # property of the operator... it works.
  382. for i, inp in enumerate(n.inputs):
  383. if inp.identifier == self.prop_remove:
  384. break
  385. else:
  386. self.report({'ERROR'}, "Input not found")
  387. return {'CANCELLED'}
  388. # it's possible that the output property's identifier isn't the
  389. # exact same... but I don' care. Shouldn't ever happen. TODO
  390. for j, out in enumerate(n.outputs):
  391. if out.identifier == self.prop_remove:
  392. break
  393. else:
  394. self.report({'ERROR'}, "Output not found")
  395. raise RuntimeError("This should not happen!")
  396. n.inputs.remove ( n.inputs [i] )
  397. n.outputs.remove( n.outputs[j] )
  398. return {'FINISHED'}
  399. # SIMPLE node operators...
  400. # May rewrite these in a more generic way later
  401. class FcurveAddKeyframeInput(bpy.types.Operator):
  402. """Add a keyframe input to the fCurve node"""
  403. bl_idname = "mantis.fcurve_node_add_kf"
  404. bl_label = "Add Keyframe"
  405. bl_options = {'INTERNAL'}
  406. @classmethod
  407. def poll(cls, context):
  408. return (hasattr(context, 'active_node') )
  409. def execute(self, context):
  410. num_keys = len( context.node.inputs)
  411. context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
  412. return {'FINISHED'}
  413. class FcurveRemoveKeyframeInput(bpy.types.Operator):
  414. """Remove a keyframe input from the fCurve node"""
  415. bl_idname = "mantis.fcurve_node_remove_kf"
  416. bl_label = "Remove Keyframe"
  417. bl_options = {'INTERNAL'}
  418. @classmethod
  419. def poll(cls, context):
  420. return (hasattr(context, 'active_node') )
  421. def execute(self, context):
  422. n = context.node
  423. n.inputs.remove(n.inputs[-1])
  424. return {'FINISHED'}
  425. class DriverAddDriverVariableInput(bpy.types.Operator):
  426. """Add a Driver Variable input to the Driver node"""
  427. bl_idname = "mantis.driver_node_add_variable"
  428. bl_label = "Add Driver Variable"
  429. bl_options = {'INTERNAL'}
  430. @classmethod
  431. def poll(cls, context):
  432. return (hasattr(context, 'active_node') )
  433. def execute(self, context): # unicode for 'a'
  434. i = len (context.node.inputs) - 2 + 96
  435. context.node.inputs.new("DriverVariableSocket", chr(i))
  436. return {'FINISHED'}
  437. class DriverRemoveDriverVariableInput(bpy.types.Operator):
  438. """Remove a DriverVariable input from the active Driver node"""
  439. bl_idname = "mantis.driver_node_remove_variable"
  440. bl_label = "Remove Driver Variable"
  441. bl_options = {'INTERNAL'}
  442. @classmethod
  443. def poll(cls, context):
  444. return (hasattr(context, 'active_node') )
  445. def execute(self, context):
  446. n = context.node
  447. n.inputs.remove(n.inputs[-1])
  448. return {'FINISHED'}
  449. class LinkArmatureAddTargetInput(bpy.types.Operator):
  450. """Add a Driver Variable input to the Driver node"""
  451. bl_idname = "mantis.link_armature_node_add_target"
  452. bl_label = "Add Target"
  453. bl_options = {'INTERNAL'}
  454. @classmethod
  455. def poll(cls, context):
  456. return hasattr(context, 'node')
  457. def execute(self, context): # unicode for 'a'
  458. num_targets = len( list(context.node.inputs)[6:])//2
  459. context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
  460. context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
  461. return {'FINISHED'}
  462. class LinkArmatureRemoveTargetInput(bpy.types.Operator):
  463. """Remove a DriverVariable input from the active Driver node"""
  464. bl_idname = "mantis.link_armature_node_remove_target"
  465. bl_label = "Remove Target"
  466. bl_options = {'INTERNAL'}
  467. @classmethod
  468. def poll(cls, context):
  469. return hasattr(context, 'node')
  470. def execute(self, context):
  471. n = context.node
  472. n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
  473. return {'FINISHED'}