ops_nodegroup.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  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 mantis_tree_poll_op(context):
  9. space = context.space_data
  10. if hasattr(space, "node_tree"):
  11. if (space.node_tree):
  12. return (space.tree_type in ["MantisTree", "SchemaTree"])
  13. return False
  14. def any_tree_poll(context):
  15. space = context.space_data
  16. if hasattr(space, "node_tree"):
  17. return True
  18. return False
  19. #########################################################################3
  20. class MantisGroupNodes(Operator):
  21. """Create node-group from selected nodes"""
  22. bl_idname = "mantis.group_nodes"
  23. bl_label = "Group Nodes"
  24. bl_options = {'REGISTER', 'UNDO'}
  25. @classmethod
  26. def poll(cls, context):
  27. return mantis_tree_poll_op(context)
  28. def execute(self, context):
  29. base_tree=context.space_data.path[-1].node_tree
  30. try:
  31. for path_item in context.space_data.path:
  32. path_item.node_tree.is_exporting = True
  33. from .i_o import export_to_json, do_import
  34. from random import random
  35. grp_name = "".join([chr(int(random()*30)+35) for i in range(20)])
  36. trees=[base_tree]
  37. selected_nodes=export_to_json(trees, write_file=False, only_selected=True)
  38. # this snippet of confusing indirection edits the name of the base tree in the JSON data
  39. selected_nodes[base_tree.name][0]["name"]=grp_name
  40. do_import(selected_nodes, context)
  41. affected_links_in = []
  42. affected_links_out = []
  43. for l in base_tree.links:
  44. if l.from_node.select and not l.to_node.select: affected_links_out.append(l)
  45. if not l.from_node.select and l.to_node.select: affected_links_in.append(l)
  46. delete_me = []
  47. all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
  48. for n in base_tree.nodes:
  49. if n.select:
  50. node_loc = (0,0,0)
  51. if bpy.app.version <= (4, 4):
  52. node_loc = n.location
  53. parent = n.parent
  54. while (parent): # accumulate parent offset
  55. node_loc += parent.location
  56. parent = parent.parent
  57. else: # there is a new location_absolute property in 4.4
  58. node_loc = n.location_absolute
  59. if node_loc.x < all_nodes_bounding_box[0].x:
  60. all_nodes_bounding_box[0].x = node_loc.x
  61. if node_loc.y < all_nodes_bounding_box[0].y:
  62. all_nodes_bounding_box[0].y = node_loc.y
  63. #
  64. if node_loc.x > all_nodes_bounding_box[1].x:
  65. all_nodes_bounding_box[1].x = node_loc.x
  66. if node_loc.y > all_nodes_bounding_box[1].y:
  67. all_nodes_bounding_box[1].y = node_loc.y
  68. delete_me.append(n)
  69. grp_node = base_tree.nodes.new('MantisNodeGroup')
  70. grp_node.node_tree = bpy.data.node_groups[grp_name]
  71. bb_center = all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1],0.5)
  72. for n in grp_node.node_tree.nodes:
  73. n.location -= bb_center
  74. 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))
  75. from .base_definitions import node_group_update
  76. grp_node.is_updating=True
  77. try:
  78. node_group_update(grp_node, force=True)
  79. finally:
  80. grp_node.is_updating=False
  81. # for each node in the JSON
  82. for n in selected_nodes[base_tree.name][2].values():
  83. for s in n["sockets"].values(): # for each socket in the node
  84. if source := s.get("source"):
  85. prGreen (s["name"], source[0], source[1])
  86. base_tree_node=base_tree.nodes.get(source[0])
  87. if s["is_output"]:
  88. for output in base_tree_node.outputs:
  89. if output.identifier == source[1]:
  90. break
  91. else:
  92. raise RuntimeError(wrapRed("Socket not found when grouping"))
  93. base_tree.links.new(input=output, output=grp_node.inputs[s["name"]])
  94. else:
  95. for s_input in base_tree_node.inputs:
  96. if s_input.identifier == source[1]:
  97. break
  98. else:
  99. raise RuntimeError(wrapRed("Socket not found when grouping"))
  100. base_tree.links.new(input=grp_node.outputs[s["name"]], output=s_input)
  101. for n in delete_me: base_tree.nodes.remove(n)
  102. base_tree.nodes.active = grp_node
  103. finally: # MAKE SURE to turn it back to not exporting
  104. for path_item in context.space_data.path:
  105. path_item.node_tree.is_exporting = False
  106. grp_node.node_tree.name = "Group_Node.000"
  107. return {'FINISHED'}
  108. class MantisEditGroup(Operator):
  109. """Edit the group referenced by the active node (or exit the current node-group)"""
  110. bl_idname = "mantis.edit_group"
  111. bl_label = "Edit Group"
  112. bl_options = {'REGISTER', 'UNDO'}
  113. @classmethod
  114. def poll(cls, context):
  115. return (
  116. mantis_tree_poll_op(context)
  117. )
  118. def execute(self, context):
  119. space = context.space_data
  120. path = space.path
  121. node = path[len(path)-1].node_tree.nodes.active
  122. base_tree = path[0].node_tree
  123. base_tree.do_live_update = False
  124. base_tree.is_executing = True
  125. try:
  126. if hasattr(node, "node_tree"):
  127. if (node.node_tree):
  128. path.append(node.node_tree, node=node)
  129. path[0].node_tree.display_update(context)
  130. return {"FINISHED"}
  131. elif len(path) > 1:
  132. path.pop()
  133. # get the active node in the current path
  134. active = path[len(path)-1].node_tree.nodes.active
  135. from .base_definitions import node_group_update
  136. active.is_updating = True
  137. try:
  138. node_group_update(active, force = True)
  139. finally:
  140. active.is_updating = False
  141. # call update to force the node group to check if its tree has changed
  142. # now we need to loop through the tree and update all node groups of this type.
  143. from .utilities import get_all_nodes_of_type
  144. for g in get_all_nodes_of_type(base_tree, "MantisNodeGroup"):
  145. if g.node_tree == active.node_tree:
  146. g.is_updating = True
  147. active.is_updating = True
  148. try:
  149. node_group_update(g, force = True)
  150. finally:
  151. g.is_updating = False
  152. active.is_updating = False
  153. base_tree.display_update(context)
  154. base_tree.is_executing = True
  155. # base_tree.is_executing = True # because it seems display_update unsets this.
  156. finally:
  157. base_tree.do_live_update = True
  158. base_tree.is_executing = False
  159. # HACK
  160. base_tree.handler_flip = True # HACK
  161. # HACK
  162. # I have no idea why but the operator finishing causes the exeuction handler to fire
  163. # I have no control over this since it happens after the execution returns...
  164. # so I have to do this ridiculous hack with a Boolean flip bit.
  165. return {"FINISHED"}
  166. class MantisNewNodeTree(Operator):
  167. """Add a new Mantis tree."""
  168. bl_idname = "mantis.new_node_tree"
  169. bl_label = "New Node Group"
  170. bl_options = {'REGISTER', 'UNDO'}
  171. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  172. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  173. @classmethod
  174. def poll(cls, context):
  175. return (
  176. mantis_tree_poll_op(context) and \
  177. context.node.bl_idname in ["MantisNodeGroup", "MantisSchemaGroup"]
  178. )
  179. @classmethod
  180. def poll(cls, context):
  181. return True #(hasattr(context, 'active_node') )
  182. def invoke(self, context, event):
  183. self.tree_invoked = context.node.id_data.name
  184. self.node_invoked = context.node.name
  185. return self.execute(context)
  186. def execute(self, context):
  187. node = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  188. if node.bl_idname == "MantisSchemaGroup":
  189. if (node.node_tree):
  190. print('a')
  191. return {"CANCELLED"}
  192. else:
  193. from bpy import data
  194. print('b')
  195. node.node_tree = data.node_groups.new(name='Schema Group', type='SchemaTree')
  196. return {'FINISHED'}
  197. elif node.bl_idname == "MantisNodeGroup":
  198. if (node.node_tree):
  199. print('c')
  200. return {"CANCELLED"}
  201. else:
  202. from bpy import data
  203. print('d')
  204. node.node_tree = data.node_groups.new(name='Mantis Group', type='MantisTree')
  205. return {'FINISHED'}
  206. else:
  207. return {"CANCELLED"}
  208. class ExecuteNodeTree(Operator):
  209. """Execute this node tree"""
  210. bl_idname = "mantis.execute_node_tree"
  211. bl_label = "Execute Node Tree"
  212. bl_options = {'REGISTER', 'UNDO'}
  213. @classmethod
  214. def poll(cls, context):
  215. return (mantis_tree_poll_op(context))
  216. def execute(self, context):
  217. from time import time
  218. from .utilities import wrapGreen
  219. tree=context.space_data.path[0].node_tree
  220. import cProfile
  221. from os import environ
  222. start_time = time()
  223. do_profile=False
  224. if environ.get("DOPROFILE"):
  225. do_profile=True
  226. pass_error = True
  227. if environ.get("DOERROR"):
  228. pass_error=False
  229. if do_profile:
  230. import pstats, io
  231. from pstats import SortKey
  232. with cProfile.Profile() as pr:
  233. tree.update_tree(context, error_popups = pass_error)
  234. tree.execute_tree(context, error_popups = pass_error)
  235. # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
  236. s = io.StringIO()
  237. sortby = SortKey.TIME
  238. # sortby = SortKey.CUMULATIVE
  239. ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
  240. ps.print_stats(20) # print the top 20
  241. print(s.getvalue())
  242. else:
  243. tree.update_tree(context, error_popups = pass_error)
  244. tree.execute_tree(context, error_popups = pass_error)
  245. prGreen("Finished executing tree in %f seconds" % (time() - start_time))
  246. return {"FINISHED"}
  247. class SelectNodesOfType(Operator):
  248. """Selects all nodes of same type as active node."""
  249. bl_idname = "mantis.select_nodes_of_type"
  250. bl_label = "Select Nodes of Same Type as Active"
  251. bl_options = {'REGISTER', 'UNDO'}
  252. @classmethod
  253. def poll(cls, context):
  254. return (any_tree_poll(context))
  255. def execute(self, context):
  256. active_node = context.active_node
  257. tree = active_node.id_data
  258. if not hasattr(active_node, "node_tree"):
  259. for node in tree.nodes:
  260. node.select = (active_node.bl_idname == node.bl_idname)
  261. else:
  262. for node in tree.nodes:
  263. node.select = (active_node.bl_idname == node.bl_idname) and (active_node.node_tree == node.node_tree)
  264. return {"FINISHED"}
  265. def get_parent_tree_interface_enum(operator, context):
  266. ret = []; i = -1
  267. tree = bpy.data.node_groups[operator.tree_invoked]
  268. for sock in tree.interface.items_tree:
  269. if sock.item_type == 'PANEL': continue
  270. if sock.in_out == "OUTPUT": continue
  271. ret.append( (sock.identifier, sock.name, "Socket from Node Group Input", i := i + 1), )
  272. return ret
  273. def get_node_inputs_enum(operator, context):
  274. ret = []; i = -1
  275. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  276. for inp in n.inputs:
  277. ret.append( (inp.identifier, inp.name, "Socket of node to connect to.", i := i + 1), )
  278. return ret
  279. class ConnectNodeToInput(Operator):
  280. """Connects a Node Group Input socket to specified socket of active node and all selected same-type nodes."""
  281. bl_idname = "mantis.connect_nodes_to_input"
  282. bl_label = "Connect Socket to Input for Selected Nodes"
  283. bl_options = {'REGISTER', 'UNDO'}
  284. group_output : bpy.props.EnumProperty(
  285. items=get_parent_tree_interface_enum,
  286. name="Node Group Input Socket",
  287. description="Select which socket from the Node Group Input to connect to this node",)
  288. node_input : bpy.props.EnumProperty(
  289. items=get_node_inputs_enum,
  290. name="Node Input Socket",
  291. description="Select which of this node's sockets to recieve the connection",)
  292. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  293. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  294. @classmethod
  295. def poll(cls, context):
  296. return (any_tree_poll(context))
  297. def invoke(self, context, event):
  298. self.tree_invoked = context.active_node.id_data.name
  299. self.node_invoked = context.active_node.name
  300. # we use active_node here ^ because we are comparing the active node to the selection.
  301. wm = context.window_manager
  302. return wm.invoke_props_dialog(self)
  303. def execute(self, context):
  304. t = bpy.data.node_groups[self.tree_invoked]
  305. if hasattr(t, "is_executing"): # for Mantis trees, but this function should just work anywhere.
  306. t.is_executing = True
  307. n = t.nodes[self.node_invoked]
  308. for node in t.nodes:
  309. if n.bl_idname == node.bl_idname and node.select:
  310. # the bl_idname is the same so they both have node_tree
  311. if hasattr(n, "node_tree") and n.node_tree != node.node_tree: continue
  312. # TODO: maybe I should try and find a nearby input node and reuse it
  313. # doing these identifier lookups again and again is slow, whatever. faster than doing it by hand
  314. for connect_to_me in node.inputs:
  315. if connect_to_me.identifier == self.node_input: break
  316. if connect_to_me.is_linked: connect_to_me = None
  317. if connect_to_me: # only make the node if the socket is there and free
  318. inp = t.nodes.new("NodeGroupInput")
  319. connect_me = None
  320. for s in inp.outputs:
  321. if s.identifier != self.group_output: s.hide = True
  322. else: connect_me = s
  323. inp.location = node.location
  324. inp.location.x-=200
  325. t.links.new(input=connect_me, output=connect_to_me)
  326. if hasattr(t, "is_executing"):
  327. t.is_executing = False
  328. return {"FINISHED"}
  329. class QueryNodeSockets(Operator):
  330. """Utility Operator for querying the data in a socket"""
  331. bl_idname = "mantis.query_sockets"
  332. bl_label = "Query Node Sockets"
  333. bl_options = {'REGISTER', 'UNDO'}
  334. @classmethod
  335. def poll(cls, context):
  336. return (mantis_tree_poll_op(context))
  337. def execute(self, context):
  338. active_node = context.active_node
  339. tree = active_node.id_data
  340. for node in tree.nodes:
  341. if not node.select: continue
  342. return {"FINISHED"}
  343. class ForceDisplayUpdate(Operator):
  344. """Utility Operator for querying the data in a socket"""
  345. bl_idname = "mantis.force_display_update"
  346. bl_label = "Force Mantis Display Update"
  347. bl_options = {'REGISTER', 'UNDO'}
  348. @classmethod
  349. def poll(cls, context):
  350. return (mantis_tree_poll_op(context))
  351. def execute(self, context):
  352. base_tree = bpy.context.space_data.path[0].node_tree
  353. base_tree.display_update(context)
  354. return {"FINISHED"}
  355. class CleanUpNodeGraph(bpy.types.Operator):
  356. """Clean Up Node Graph"""
  357. bl_idname = "mantis.nodes_cleanup"
  358. bl_label = "Clean Up Node Graph"
  359. bl_options = {'REGISTER', 'UNDO'}
  360. # num_iterations=bpy.props.IntProperty(default=8)
  361. @classmethod
  362. def poll(cls, context):
  363. return hasattr(context, 'active_node')
  364. def execute(self, context):
  365. base_tree=context.space_data.path[-1].node_tree
  366. from .utilities import SugiyamaGraph
  367. SugiyamaGraph(base_tree, 12)
  368. return {'FINISHED'}
  369. class MantisMuteNode(Operator):
  370. """Mantis Test Operator"""
  371. bl_idname = "mantis.mute_node"
  372. bl_label = "Mute Node"
  373. bl_options = {'REGISTER', 'UNDO'}
  374. @classmethod
  375. def poll(cls, context):
  376. return (mantis_tree_poll_op(context))
  377. def execute(self, context):
  378. path = context.space_data.path
  379. node = path[len(path)-1].node_tree.nodes.active
  380. node.mute = not node.mute
  381. # There should only be one of these
  382. if (enable := node.inputs.get("Enable")):
  383. # annoyingly, 'mute' and 'enable' are opposites
  384. enable.default_value = not node.mute
  385. if (hide := node.inputs.get("Hide")):
  386. hide.default_value = node.mute
  387. return {"FINISHED"}
  388. ePropertyType =(
  389. ('BOOL' , "Boolean", "Boolean", 0),
  390. ('INT' , "Integer", "Integer", 1),
  391. ('FLOAT' , "Float" , "Float" , 2),
  392. ('VECTOR', "Vector" , "Vector" , 3),
  393. ('STRING', "String" , "String" , 4),
  394. #('ENUM' , "Enum" , "Enum" , 5),
  395. )
  396. from .base_definitions import xFormNode
  397. class AddCustomProperty(bpy.types.Operator):
  398. """Add Custom Property to xForm Node"""
  399. bl_idname = "mantis.add_custom_property"
  400. bl_label = "Add Custom Property"
  401. bl_options = {'REGISTER', 'UNDO'}
  402. prop_type : bpy.props.EnumProperty(
  403. items=ePropertyType,
  404. name="New Property Type",
  405. description="Type of data for new Property",
  406. default = 'BOOL',)
  407. prop_name : bpy.props.StringProperty(default='Prop')
  408. min:bpy.props.FloatProperty(default = 0)
  409. max:bpy.props.FloatProperty(default = 1)
  410. soft_min:bpy.props.FloatProperty(default = 0)
  411. soft_max:bpy.props.FloatProperty(default = 1)
  412. description:bpy.props.StringProperty(default = "")
  413. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  414. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  415. @classmethod
  416. def poll(cls, context):
  417. return True #( hasattr(context, 'node') )
  418. def invoke(self, context, event):
  419. self.tree_invoked = context.node.id_data.name
  420. self.node_invoked = context.node.name
  421. wm = context.window_manager
  422. return wm.invoke_props_dialog(self)
  423. def execute(self, context):
  424. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  425. # For whatever reason, context.node doesn't exist anymore
  426. # (probably because I use a window to execute)
  427. # so as a sort of dumb workaround I am saving it to a hidden
  428. # property of the operator... it works but Blender complains.
  429. socktype = ''
  430. if not (self.prop_name):
  431. self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")
  432. return {'CANCELLED'}
  433. if self.prop_type == 'BOOL':
  434. socktype = 'ParameterBoolSocket'
  435. if self.prop_type == 'INT':
  436. socktype = 'ParameterIntSocket'
  437. if self.prop_type == 'FLOAT':
  438. socktype = 'ParameterFloatSocket'
  439. if self.prop_type == 'VECTOR':
  440. socktype = 'ParameterVectorSocket'
  441. if self.prop_type == 'STRING':
  442. socktype = 'ParameterStringSocket'
  443. #if self.prop_type == 'ENUM':
  444. # sock_type = 'ParameterStringSocket'
  445. if (s := n.inputs.get(self.prop_name)):
  446. try:
  447. number = int(self.prop_name[-3:])
  448. # see if it has a number
  449. number+=1
  450. self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
  451. except ValueError:
  452. self.prop_name+='.001'
  453. # WRONG # HACK # TODO # BUG #
  454. new_prop = n.inputs.new( socktype, self.prop_name)
  455. if self.prop_type in ['INT','FLOAT']:
  456. new_prop.min = self.min
  457. new_prop.max = self.max
  458. new_prop.soft_min = self.soft_min
  459. new_prop.soft_max = self.soft_max
  460. new_prop.description = self.description
  461. # now do the output
  462. n.outputs.new( socktype, self.prop_name)
  463. return {'FINISHED'}
  464. def main_get_existing_custom_properties(operator, context):
  465. ret = []; i = -1
  466. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  467. for inp in n.inputs:
  468. if 'Parameter' in inp.bl_idname:
  469. ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )
  470. return ret
  471. class EditCustomProperty(bpy.types.Operator):
  472. """Edit Custom Property"""
  473. bl_idname = "mantis.edit_custom_property"
  474. bl_label = "Edit Custom Property"
  475. bl_options = {'REGISTER', 'UNDO'}
  476. def get_existing_custom_properties(self, context):
  477. return main_get_existing_custom_properties(self, context)
  478. prop_edit : bpy.props.EnumProperty(
  479. items=get_existing_custom_properties,
  480. name="Property to Edit?",
  481. description="Select which property to edit",)
  482. prop_type : bpy.props.EnumProperty(
  483. items=ePropertyType,
  484. name="New Property Type",
  485. description="Type of data for new Property",
  486. default = 'BOOL',)
  487. prop_name : bpy.props.StringProperty(default='Prop')
  488. min:bpy.props.FloatProperty(default = 0)
  489. max:bpy.props.FloatProperty(default = 1)
  490. soft_min:bpy.props.FloatProperty(default = 0)
  491. soft_max:bpy.props.FloatProperty(default = 1)
  492. description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
  493. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  494. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  495. @classmethod
  496. def poll(cls, context):
  497. return True #( hasattr(context, 'node') )
  498. def invoke(self, context, event):
  499. self.tree_invoked = context.node.id_data.name
  500. self.node_invoked = context.node.name
  501. wm = context.window_manager
  502. return wm.invoke_props_dialog(self)
  503. def execute(self, context):
  504. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  505. prop = n.inputs.get( self.prop_edit )
  506. if prop:
  507. prop.name = self.prop_name
  508. if (s := n.inputs.get(self.prop_edit)):
  509. if self.prop_type in ['INT','FLOAT']:
  510. prop.min = self.min
  511. prop.max = self.max
  512. prop.soft_min = self.soft_min
  513. prop.soft_max = self.soft_max
  514. prop.description = self.description
  515. return {'FINISHED'}
  516. else:
  517. self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")
  518. class RemoveCustomProperty(bpy.types.Operator):
  519. """Remove a Custom Property from an xForm Node"""
  520. bl_idname = "mantis.remove_custom_property"
  521. bl_label = "Remove Custom Property"
  522. bl_options = {'REGISTER', 'UNDO'}
  523. def get_existing_custom_properties(self, context):
  524. return main_get_existing_custom_properties(self, context)
  525. prop_remove : bpy.props.EnumProperty(
  526. items=get_existing_custom_properties,
  527. name="Property to remove?",
  528. description="Select which property to remove",)
  529. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  530. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  531. @classmethod
  532. def poll(cls, context):
  533. return True #(hasattr(context, 'active_node') )
  534. def invoke(self, context, event):
  535. self.tree_invoked = context.node.id_data.name
  536. self.node_invoked = context.node.name
  537. t = context.node.id_data
  538. # HACK the props dialog makes this necesary
  539. # because context.node only exists during the event that
  540. # was created by clicking on the node.
  541. t.nodes.active = context.node # HACK
  542. context.node.select = True # HACK
  543. # I need this bc of the callback for the enum property.
  544. # for whatever reason I can't use tree_invoked there
  545. wm = context.window_manager
  546. return wm.invoke_props_dialog(self)
  547. def execute(self, context):
  548. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  549. # For whatever reason, context.node doesn't exist anymore
  550. # (probably because I use a window to execute)
  551. # so as a sort of dumb workaround I am saving it to a hidden
  552. # property of the operator... it works.
  553. for i, inp in enumerate(n.inputs):
  554. if inp.identifier == self.prop_remove:
  555. break
  556. else:
  557. self.report({'ERROR'}, "Input not found")
  558. return {'CANCELLED'}
  559. # it's possible that the output property's identifier isn't the
  560. # exact same... but I don' care. Shouldn't ever happen. TODO
  561. for j, out in enumerate(n.outputs):
  562. if out.identifier == self.prop_remove:
  563. break
  564. else:
  565. self.report({'ERROR'}, "Output not found")
  566. raise RuntimeError("This should not happen!")
  567. n.inputs.remove ( n.inputs [i] )
  568. n.outputs.remove( n.outputs[j] )
  569. return {'FINISHED'}
  570. # SIMPLE node operators...
  571. # May rewrite these in a more generic way later
  572. class FcurveAddKeyframeInput(bpy.types.Operator):
  573. """Add a keyframe input to the fCurve node"""
  574. bl_idname = "mantis.fcurve_node_add_kf"
  575. bl_label = "Add Keyframe"
  576. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  577. @classmethod
  578. def poll(cls, context):
  579. return (hasattr(context, 'active_node') )
  580. def execute(self, context):
  581. num_keys = len( context.node.inputs)-1
  582. context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
  583. return {'FINISHED'}
  584. class FcurveRemoveKeyframeInput(bpy.types.Operator):
  585. """Remove a keyframe input from the fCurve node"""
  586. bl_idname = "mantis.fcurve_node_remove_kf"
  587. bl_label = "Remove Keyframe"
  588. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  589. @classmethod
  590. def poll(cls, context):
  591. return (hasattr(context, 'active_node') )
  592. def execute(self, context):
  593. n = context.node
  594. n.inputs.remove(n.inputs[-1])
  595. return {'FINISHED'}
  596. class DriverAddDriverVariableInput(bpy.types.Operator):
  597. """Add a Driver Variable input to the Driver node"""
  598. bl_idname = "mantis.driver_node_add_variable"
  599. bl_label = "Add Driver Variable"
  600. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  601. @classmethod
  602. def poll(cls, context):
  603. return (hasattr(context, 'active_node') )
  604. def execute(self, context): # unicode for 'a'
  605. i = len (context.node.inputs) - 2 + 96
  606. context.node.inputs.new("DriverVariableSocket", chr(i))
  607. return {'FINISHED'}
  608. class DriverRemoveDriverVariableInput(bpy.types.Operator):
  609. """Remove a DriverVariable input from the active Driver node"""
  610. bl_idname = "mantis.driver_node_remove_variable"
  611. bl_label = "Remove Driver Variable"
  612. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  613. @classmethod
  614. def poll(cls, context):
  615. return (hasattr(context, 'active_node') )
  616. def execute(self, context):
  617. n = context.node
  618. n.inputs.remove(n.inputs[-1])
  619. return {'FINISHED'}
  620. class LinkArmatureAddTargetInput(bpy.types.Operator):
  621. """Add a Driver Variable input to the Driver node"""
  622. bl_idname = "mantis.link_armature_node_add_target"
  623. bl_label = "Add Target"
  624. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  625. @classmethod
  626. def poll(cls, context):
  627. return hasattr(context, 'node')
  628. def execute(self, context): # unicode for 'a'
  629. num_targets = len( list(context.node.inputs)[6:])//2
  630. context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
  631. context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
  632. return {'FINISHED'}
  633. class LinkArmatureRemoveTargetInput(bpy.types.Operator):
  634. """Remove a DriverVariable input from the active Driver node"""
  635. bl_idname = "mantis.link_armature_node_remove_target"
  636. bl_label = "Remove Target"
  637. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  638. @classmethod
  639. def poll(cls, context):
  640. return hasattr(context, 'node')
  641. def execute(self, context):
  642. n = context.node
  643. n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
  644. return {'FINISHED'}
  645. def get_socket_enum(operator, context):
  646. valid_types = []; i = -1
  647. from .socket_definitions import TellClasses, MantisSocket
  648. for cls in TellClasses():
  649. if cls.is_valid_interface_type:
  650. valid_types.append( (cls.bl_idname, cls.bl_label, "Socket Type", i := i + 1), )
  651. return valid_types
  652. class B4_4_0_Workaround_NodeTree_Interface_Update(Operator):
  653. """Selects all nodes of same type as active node."""
  654. bl_idname = "mantis.node_tree_interface_update_4_4_0_workaround"
  655. bl_label = "Add Socket to Node Tree"
  656. bl_options = {'REGISTER', 'UNDO'}
  657. socket_name : bpy.props.StringProperty()
  658. output : bpy.props.BoolProperty()
  659. socket_type : bpy.props.EnumProperty(
  660. name="Socket Type",
  661. description="Socket Type",
  662. items=get_socket_enum,
  663. default=0,)
  664. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  665. @classmethod
  666. def poll(cls, context):
  667. return (any_tree_poll(context))
  668. def invoke(self, context, event):
  669. self.tree_invoked = context.active_node.id_data.name
  670. # we use active_node here ^ because we are comparing the active node to the selection.
  671. wm = context.window_manager
  672. return wm.invoke_props_dialog(self)
  673. def execute(self, context):
  674. tree = bpy.data.node_groups[self.tree_invoked]
  675. in_out = 'OUTPUT' if self.output else 'INPUT'
  676. tree.interface.new_socket(self.socket_name, in_out=in_out, socket_type=self.socket_type)
  677. # try to prevent the next execution
  678. # because updating the interface triggers a depsgraph update.
  679. # this doesn't actually work though...TODO
  680. if tree.bl_idname == "MantisTree":
  681. tree.prevent_next_exec=True
  682. return {"FINISHED"}
  683. class ConvertBezierCurveToNURBS(Operator):
  684. """Converts all bezier splines of curve to NURBS."""
  685. bl_idname = "mantis.convert_bezcrv_to_nurbs"
  686. bl_label = "Convert Bezier Curve to NURBS"
  687. bl_options = {'REGISTER', 'UNDO'}
  688. @classmethod
  689. def poll(cls, context):
  690. return (context.active_object is not None) and (context.active_object.type=='CURVE')
  691. def execute(self, context):
  692. from .utilities import nurbs_copy_bez_spline
  693. curve = context.active_object
  694. bez=[]
  695. for spl in curve.data.splines:
  696. if spl.type=='BEZIER':
  697. bez.append(spl)
  698. for bez_spline in bez:
  699. new_spline=nurbs_copy_bez_spline(curve, bez_spline)
  700. curve.data.splines.remove(bez_spline)
  701. return {"FINISHED"}
  702. # this has to be down here for some reason. what a pain
  703. classes = [
  704. MantisGroupNodes,
  705. MantisEditGroup,
  706. MantisNewNodeTree,
  707. ExecuteNodeTree,
  708. # CreateMetaGroup,
  709. QueryNodeSockets,
  710. ForceDisplayUpdate,
  711. CleanUpNodeGraph,
  712. MantisMuteNode,
  713. SelectNodesOfType,
  714. ConnectNodeToInput,
  715. # xForm
  716. AddCustomProperty,
  717. EditCustomProperty,
  718. RemoveCustomProperty,
  719. # EditFCurveNode,
  720. FcurveAddKeyframeInput,
  721. FcurveRemoveKeyframeInput,
  722. # Driver
  723. DriverAddDriverVariableInput,
  724. DriverRemoveDriverVariableInput,
  725. # Armature Link Node
  726. LinkArmatureAddTargetInput,
  727. LinkArmatureRemoveTargetInput,
  728. # rigging utilities
  729. ConvertBezierCurveToNURBS,
  730. ]
  731. if (bpy.app.version >= (4, 4, 0)):
  732. classes.append(B4_4_0_Workaround_NodeTree_Interface_Update)
  733. def TellClasses():
  734. return classes