ops_nodegroup.py 21 KB

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