ops_nodegroup.py 22 KB

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