ops_nodegroup.py 21 KB

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