ops_nodegroup.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  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. live_update_start_state=base_tree.do_live_update
  124. base_tree.do_live_update = False
  125. base_tree.is_executing = True
  126. try:
  127. if hasattr(node, "node_tree"):
  128. if (node.node_tree):
  129. path.append(node.node_tree, node=node)
  130. path[0].node_tree.display_update(context)
  131. return {"FINISHED"}
  132. elif len(path) > 1:
  133. path.pop()
  134. # get the active node in the current path
  135. active = path[len(path)-1].node_tree.nodes.active
  136. from .base_definitions import node_group_update
  137. active.is_updating = True
  138. try:
  139. node_group_update(active, force = True)
  140. finally:
  141. active.is_updating = False
  142. # call update to force the node group to check if its tree has changed
  143. # now we need to loop through the tree and update all node groups of this type.
  144. from .utilities import get_all_nodes_of_type
  145. for g in get_all_nodes_of_type(base_tree, "MantisNodeGroup"):
  146. if g.node_tree == active.node_tree:
  147. g.is_updating = True
  148. active.is_updating = True
  149. try:
  150. node_group_update(g, force = True)
  151. finally:
  152. g.is_updating = False
  153. active.is_updating = False
  154. base_tree.display_update(context)
  155. base_tree.is_executing = True
  156. # base_tree.is_executing = True # because it seems display_update unsets this.
  157. finally:
  158. base_tree.do_live_update = live_update_start_state
  159. base_tree.is_executing = False
  160. # HACK
  161. base_tree.handler_flip = True # HACK
  162. # HACK
  163. # I have no idea why but the operator finishing causes the exeuction handler to fire
  164. # I have no control over this since it happens after the execution returns...
  165. # so I have to do this ridiculous hack with a Boolean flip bit.
  166. return {"FINISHED"}
  167. class MantisNewNodeTree(Operator):
  168. """Add a new Mantis tree."""
  169. bl_idname = "mantis.new_node_tree"
  170. bl_label = "New Node Group"
  171. bl_options = {'REGISTER', 'UNDO'}
  172. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  173. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  174. @classmethod
  175. def poll(cls, context):
  176. return (
  177. mantis_tree_poll_op(context) and \
  178. context.node.bl_idname in ["MantisNodeGroup", "MantisSchemaGroup"]
  179. )
  180. @classmethod
  181. def poll(cls, context):
  182. return True #(hasattr(context, 'active_node') )
  183. def invoke(self, context, event):
  184. self.tree_invoked = context.node.id_data.name
  185. self.node_invoked = context.node.name
  186. return self.execute(context)
  187. def execute(self, context):
  188. node = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  189. if node.bl_idname == "MantisSchemaGroup":
  190. if (node.node_tree):
  191. print('a')
  192. return {"CANCELLED"}
  193. else:
  194. from bpy import data
  195. print('b')
  196. node.node_tree = data.node_groups.new(name='Schema Group', type='SchemaTree')
  197. return {'FINISHED'}
  198. elif node.bl_idname == "MantisNodeGroup":
  199. if (node.node_tree):
  200. print('c')
  201. return {"CANCELLED"}
  202. else:
  203. from bpy import data
  204. print('d')
  205. node.node_tree = data.node_groups.new(name='Mantis Group', type='MantisTree')
  206. return {'FINISHED'}
  207. else:
  208. return {"CANCELLED"}
  209. class InvalidateNodeTree(Operator):
  210. """Invalidates this node tree, forcing it to read all data again."""
  211. bl_idname = "mantis.invalidate_node_tree"
  212. bl_label = "Clear Node Tree Cache"
  213. bl_options = {'REGISTER', 'UNDO'}
  214. @classmethod
  215. def poll(cls, context):
  216. return (mantis_tree_poll_op(context))
  217. def execute(self, context):
  218. print("Clearing Active Node Tree cache")
  219. tree=context.space_data.path[0].node_tree
  220. tree.execution_id=''; tree.hash=''
  221. return {"FINISHED"}
  222. class ExecuteNodeTree(Operator):
  223. """Execute this node tree"""
  224. bl_idname = "mantis.execute_node_tree"
  225. bl_label = "Execute Node Tree"
  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. from time import time
  232. from .utilities import wrapGreen
  233. tree=context.space_data.path[0].node_tree
  234. import cProfile
  235. from os import environ
  236. start_time = time()
  237. do_profile=False
  238. if environ.get("DOPROFILE"):
  239. do_profile=True
  240. pass_error = True
  241. if environ.get("DOERROR"):
  242. pass_error=False
  243. if do_profile:
  244. import pstats, io
  245. from pstats import SortKey
  246. with cProfile.Profile() as pr:
  247. tree.update_tree(context, error_popups = pass_error)
  248. tree.execute_tree(context, error_popups = pass_error)
  249. # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
  250. s = io.StringIO()
  251. sortby = SortKey.TIME
  252. # sortby = SortKey.CUMULATIVE
  253. ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
  254. ps.print_stats(20) # print the top 20
  255. print(s.getvalue())
  256. else:
  257. tree.update_tree(context, error_popups = pass_error)
  258. tree.execute_tree(context, error_popups = pass_error)
  259. prGreen("Finished executing tree in %f seconds" % (time() - start_time))
  260. return {"FINISHED"}
  261. class SelectNodesOfType(Operator):
  262. """Selects all nodes of same type as active node."""
  263. bl_idname = "mantis.select_nodes_of_type"
  264. bl_label = "Select Nodes of Same Type as Active"
  265. bl_options = {'REGISTER', 'UNDO'}
  266. @classmethod
  267. def poll(cls, context):
  268. return (any_tree_poll(context))
  269. def execute(self, context):
  270. active_node = context.active_node
  271. tree = active_node.id_data
  272. if not hasattr(active_node, "node_tree"):
  273. for node in tree.nodes:
  274. node.select = (active_node.bl_idname == node.bl_idname)
  275. else:
  276. for node in tree.nodes:
  277. node.select = (active_node.bl_idname == node.bl_idname) and (active_node.node_tree == node.node_tree)
  278. return {"FINISHED"}
  279. def get_parent_tree_interface_enum(operator, context):
  280. ret = []; i = -1
  281. tree = bpy.data.node_groups[operator.tree_invoked]
  282. for sock in tree.interface.items_tree:
  283. if sock.item_type == 'PANEL': continue
  284. if sock.in_out == "OUTPUT": continue
  285. ret.append( (sock.identifier, sock.name, "Socket from Node Group Input", i := i + 1), )
  286. return ret
  287. def get_node_inputs_enum(operator, context):
  288. ret = []; i = -1
  289. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  290. for inp in n.inputs:
  291. ret.append( (inp.identifier, inp.name, "Socket of node to connect to.", i := i + 1), )
  292. return ret
  293. class ConnectNodeToInput(Operator):
  294. """Connects a Node Group Input socket to specified socket of active node and all selected same-type nodes."""
  295. bl_idname = "mantis.connect_nodes_to_input"
  296. bl_label = "Connect Socket to Input for Selected Nodes"
  297. bl_options = {'REGISTER', 'UNDO'}
  298. group_output : bpy.props.EnumProperty(
  299. items=get_parent_tree_interface_enum,
  300. name="Node Group Input Socket",
  301. description="Select which socket from the Node Group Input to connect to this node",)
  302. node_input : bpy.props.EnumProperty(
  303. items=get_node_inputs_enum,
  304. name="Node Input Socket",
  305. description="Select which of this node's sockets to recieve the connection",)
  306. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  307. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  308. @classmethod
  309. def poll(cls, context):
  310. return (any_tree_poll(context))
  311. def invoke(self, context, event):
  312. self.tree_invoked = context.active_node.id_data.name
  313. self.node_invoked = context.active_node.name
  314. # we use active_node here ^ because we are comparing the active node to the selection.
  315. wm = context.window_manager
  316. return wm.invoke_props_dialog(self)
  317. def execute(self, context):
  318. t = bpy.data.node_groups[self.tree_invoked]
  319. if hasattr(t, "is_executing"): # for Mantis trees, but this function should just work anywhere.
  320. t.is_executing = True
  321. n = t.nodes[self.node_invoked]
  322. for node in t.nodes:
  323. if n.bl_idname == node.bl_idname and node.select:
  324. # the bl_idname is the same so they both have node_tree
  325. if hasattr(n, "node_tree") and n.node_tree != node.node_tree: continue
  326. # TODO: maybe I should try and find a nearby input node and reuse it
  327. # doing these identifier lookups again and again is slow, whatever. faster than doing it by hand
  328. for connect_to_me in node.inputs:
  329. if connect_to_me.identifier == self.node_input: break
  330. if connect_to_me.is_linked: connect_to_me = None
  331. if connect_to_me: # only make the node if the socket is there and free
  332. inp = t.nodes.new("NodeGroupInput")
  333. connect_me = None
  334. for s in inp.outputs:
  335. if s.identifier != self.group_output: s.hide = True
  336. else: connect_me = s
  337. inp.location = node.location
  338. inp.location.x-=200
  339. t.links.new(input=connect_me, output=connect_to_me)
  340. if hasattr(t, "is_executing"):
  341. t.is_executing = False
  342. return {"FINISHED"}
  343. class QueryNodeSockets(Operator):
  344. """Utility Operator for querying the data in a socket"""
  345. bl_idname = "mantis.query_sockets"
  346. bl_label = "Query Node Sockets"
  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. active_node = context.active_node
  353. tree = active_node.id_data
  354. for node in tree.nodes:
  355. if not node.select: continue
  356. return {"FINISHED"}
  357. class ForceDisplayUpdate(Operator):
  358. """Utility Operator for querying the data in a socket"""
  359. bl_idname = "mantis.force_display_update"
  360. bl_label = "Force Mantis Display Update"
  361. bl_options = {'REGISTER', 'UNDO'}
  362. @classmethod
  363. def poll(cls, context):
  364. return (mantis_tree_poll_op(context))
  365. def execute(self, context):
  366. base_tree = bpy.context.space_data.path[0].node_tree
  367. base_tree.display_update(context)
  368. return {"FINISHED"}
  369. class CleanUpNodeGraph(bpy.types.Operator):
  370. """Clean Up Node Graph"""
  371. bl_idname = "mantis.nodes_cleanup"
  372. bl_label = "Clean Up Node Graph"
  373. bl_options = {'REGISTER', 'UNDO'}
  374. # num_iterations=bpy.props.IntProperty(default=8)
  375. @classmethod
  376. def poll(cls, context):
  377. return hasattr(context, 'active_node')
  378. def execute(self, context):
  379. base_tree=context.space_data.path[-1].node_tree
  380. from .utilities import SugiyamaGraph
  381. SugiyamaGraph(base_tree, 12)
  382. return {'FINISHED'}
  383. class MantisMuteNode(Operator):
  384. """Mantis Test Operator"""
  385. bl_idname = "mantis.mute_node"
  386. bl_label = "Mute Node"
  387. bl_options = {'REGISTER', 'UNDO'}
  388. @classmethod
  389. def poll(cls, context):
  390. return (mantis_tree_poll_op(context))
  391. def execute(self, context):
  392. path = context.space_data.path
  393. node = path[len(path)-1].node_tree.nodes.active
  394. node.mute = not node.mute
  395. # There should only be one of these
  396. if (enable := node.inputs.get("Enable")):
  397. # annoyingly, 'mute' and 'enable' are opposites
  398. enable.default_value = not node.mute
  399. # this one is for Deformers
  400. elif (enable := node.inputs.get("Enable in Viewport")):
  401. # annoyingly, 'mute' and 'enable' are opposites
  402. enable.default_value = not node.mute
  403. elif (hide := node.inputs.get("Hide")):
  404. hide.default_value = node.mute
  405. elif (hide := node.inputs.get("Hide in Viewport")):
  406. hide.default_value = node.mute
  407. return {"FINISHED"}
  408. ePropertyType =(
  409. ('BOOL' , "Boolean", "Boolean", 0),
  410. ('INT' , "Integer", "Integer", 1),
  411. ('FLOAT' , "Float" , "Float" , 2),
  412. ('VECTOR', "Vector" , "Vector" , 3),
  413. ('STRING', "String" , "String" , 4),
  414. #('ENUM' , "Enum" , "Enum" , 5),
  415. )
  416. from .base_definitions import xFormNode
  417. class AddCustomProperty(bpy.types.Operator):
  418. """Add Custom Property to xForm Node"""
  419. bl_idname = "mantis.add_custom_property"
  420. bl_label = "Add Custom Property"
  421. bl_options = {'REGISTER', 'UNDO'}
  422. prop_type : bpy.props.EnumProperty(
  423. items=ePropertyType,
  424. name="New Property Type",
  425. description="Type of data for new Property",
  426. default = 'BOOL',)
  427. prop_name : bpy.props.StringProperty(default='Prop')
  428. min:bpy.props.FloatProperty(default = 0)
  429. max:bpy.props.FloatProperty(default = 1)
  430. soft_min:bpy.props.FloatProperty(default = 0)
  431. soft_max:bpy.props.FloatProperty(default = 1)
  432. description:bpy.props.StringProperty(default = "")
  433. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  434. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  435. @classmethod
  436. def poll(cls, context):
  437. return True #( hasattr(context, 'node') )
  438. def invoke(self, context, event):
  439. self.tree_invoked = context.node.id_data.name
  440. self.node_invoked = context.node.name
  441. wm = context.window_manager
  442. return wm.invoke_props_dialog(self)
  443. def execute(self, context):
  444. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  445. # For whatever reason, context.node doesn't exist anymore
  446. # (probably because I use a window to execute)
  447. # so as a sort of dumb workaround I am saving it to a hidden
  448. # property of the operator... it works but Blender complains.
  449. socktype = ''
  450. if not (self.prop_name):
  451. self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")
  452. return {'CANCELLED'}
  453. if self.prop_type == 'BOOL':
  454. socktype = 'ParameterBoolSocket'
  455. if self.prop_type == 'INT':
  456. socktype = 'ParameterIntSocket'
  457. if self.prop_type == 'FLOAT':
  458. socktype = 'ParameterFloatSocket'
  459. if self.prop_type == 'VECTOR':
  460. socktype = 'ParameterVectorSocket'
  461. if self.prop_type == 'STRING':
  462. socktype = 'ParameterStringSocket'
  463. #if self.prop_type == 'ENUM':
  464. # sock_type = 'ParameterStringSocket'
  465. if (s := n.inputs.get(self.prop_name)):
  466. try:
  467. number = int(self.prop_name[-3:])
  468. # see if it has a number
  469. number+=1
  470. self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
  471. except ValueError:
  472. self.prop_name+='.001'
  473. # WRONG # HACK # TODO # BUG #
  474. new_prop = n.inputs.new( socktype, self.prop_name)
  475. if self.prop_type in ['INT','FLOAT']:
  476. new_prop.min = self.min
  477. new_prop.max = self.max
  478. new_prop.soft_min = self.soft_min
  479. new_prop.soft_max = self.soft_max
  480. new_prop.description = self.description
  481. # now do the output
  482. n.outputs.new( socktype, self.prop_name)
  483. return {'FINISHED'}
  484. def main_get_existing_custom_properties(operator, context):
  485. ret = []; i = -1
  486. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  487. for inp in n.inputs:
  488. if 'Parameter' in inp.bl_idname:
  489. ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )
  490. return ret
  491. class EditCustomProperty(bpy.types.Operator):
  492. """Edit Custom Property"""
  493. bl_idname = "mantis.edit_custom_property"
  494. bl_label = "Edit Custom Property"
  495. bl_options = {'REGISTER', 'UNDO'}
  496. def get_existing_custom_properties(self, context):
  497. return main_get_existing_custom_properties(self, context)
  498. prop_edit : bpy.props.EnumProperty(
  499. items=get_existing_custom_properties,
  500. name="Property to Edit?",
  501. description="Select which property to edit",)
  502. prop_type : bpy.props.EnumProperty(
  503. items=ePropertyType,
  504. name="New Property Type",
  505. description="Type of data for new Property",
  506. default = 'BOOL',)
  507. prop_name : bpy.props.StringProperty(default='Prop')
  508. min:bpy.props.FloatProperty(default = 0)
  509. max:bpy.props.FloatProperty(default = 1)
  510. soft_min:bpy.props.FloatProperty(default = 0)
  511. soft_max:bpy.props.FloatProperty(default = 1)
  512. description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
  513. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  514. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  515. @classmethod
  516. def poll(cls, context):
  517. return True #( hasattr(context, 'node') )
  518. def invoke(self, context, event):
  519. self.tree_invoked = context.node.id_data.name
  520. self.node_invoked = context.node.name
  521. wm = context.window_manager
  522. return wm.invoke_props_dialog(self)
  523. def execute(self, context):
  524. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  525. prop = n.inputs.get( self.prop_edit )
  526. if prop:
  527. prop.name = self.prop_name
  528. if (s := n.inputs.get(self.prop_edit)):
  529. if self.prop_type in ['INT','FLOAT']:
  530. prop.min = self.min
  531. prop.max = self.max
  532. prop.soft_min = self.soft_min
  533. prop.soft_max = self.soft_max
  534. prop.description = self.description
  535. return {'FINISHED'}
  536. else:
  537. self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")
  538. class RemoveCustomProperty(bpy.types.Operator):
  539. """Remove a Custom Property from an xForm Node"""
  540. bl_idname = "mantis.remove_custom_property"
  541. bl_label = "Remove Custom Property"
  542. bl_options = {'REGISTER', 'UNDO'}
  543. def get_existing_custom_properties(self, context):
  544. return main_get_existing_custom_properties(self, context)
  545. prop_remove : bpy.props.EnumProperty(
  546. items=get_existing_custom_properties,
  547. name="Property to remove?",
  548. description="Select which property to remove",)
  549. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  550. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  551. @classmethod
  552. def poll(cls, context):
  553. return True #(hasattr(context, 'active_node') )
  554. def invoke(self, context, event):
  555. self.tree_invoked = context.node.id_data.name
  556. self.node_invoked = context.node.name
  557. t = context.node.id_data
  558. # HACK the props dialog makes this necesary
  559. # because context.node only exists during the event that
  560. # was created by clicking on the node.
  561. t.nodes.active = context.node # HACK
  562. context.node.select = True # HACK
  563. # I need this bc of the callback for the enum property.
  564. # for whatever reason I can't use tree_invoked there
  565. wm = context.window_manager
  566. return wm.invoke_props_dialog(self)
  567. def execute(self, context):
  568. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  569. # For whatever reason, context.node doesn't exist anymore
  570. # (probably because I use a window to execute)
  571. # so as a sort of dumb workaround I am saving it to a hidden
  572. # property of the operator... it works.
  573. for i, inp in enumerate(n.inputs):
  574. if inp.identifier == self.prop_remove:
  575. break
  576. else:
  577. self.report({'ERROR'}, "Input not found")
  578. return {'CANCELLED'}
  579. # it's possible that the output property's identifier isn't the
  580. # exact same... but I don' care. Shouldn't ever happen. TODO
  581. for j, out in enumerate(n.outputs):
  582. if out.identifier == self.prop_remove:
  583. break
  584. else:
  585. self.report({'ERROR'}, "Output not found")
  586. raise RuntimeError("This should not happen!")
  587. n.inputs.remove ( n.inputs [i] )
  588. n.outputs.remove( n.outputs[j] )
  589. return {'FINISHED'}
  590. # SIMPLE node operators...
  591. # May rewrite these in a more generic way later
  592. class FcurveAddKeyframeInput(bpy.types.Operator):
  593. """Add a keyframe input to the fCurve node"""
  594. bl_idname = "mantis.fcurve_node_add_kf"
  595. bl_label = "Add Keyframe"
  596. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  597. @classmethod
  598. def poll(cls, context):
  599. return (hasattr(context, 'active_node') )
  600. def execute(self, context):
  601. num_keys = len( context.node.inputs)-1
  602. context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
  603. return {'FINISHED'}
  604. class FcurveRemoveKeyframeInput(bpy.types.Operator):
  605. """Remove a keyframe input from the fCurve node"""
  606. bl_idname = "mantis.fcurve_node_remove_kf"
  607. bl_label = "Remove Keyframe"
  608. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  609. @classmethod
  610. def poll(cls, context):
  611. return (hasattr(context, 'active_node') )
  612. def execute(self, context):
  613. n = context.node
  614. n.inputs.remove(n.inputs[-1])
  615. return {'FINISHED'}
  616. class DriverAddDriverVariableInput(bpy.types.Operator):
  617. """Add a Driver Variable input to the Driver node"""
  618. bl_idname = "mantis.driver_node_add_variable"
  619. bl_label = "Add Driver Variable"
  620. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  621. @classmethod
  622. def poll(cls, context):
  623. return (hasattr(context, 'active_node') )
  624. def execute(self, context): # unicode for 'a'
  625. i = len (context.node.inputs) - 2 + 96
  626. context.node.inputs.new("DriverVariableSocket", chr(i))
  627. return {'FINISHED'}
  628. class DriverRemoveDriverVariableInput(bpy.types.Operator):
  629. """Remove a DriverVariable input from the active Driver node"""
  630. bl_idname = "mantis.driver_node_remove_variable"
  631. bl_label = "Remove Driver Variable"
  632. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  633. @classmethod
  634. def poll(cls, context):
  635. return (hasattr(context, 'active_node') )
  636. def execute(self, context):
  637. n = context.node
  638. n.inputs.remove(n.inputs[-1])
  639. return {'FINISHED'}
  640. class LinkArmatureAddTargetInput(bpy.types.Operator):
  641. """Add a Driver Variable input to the Driver node"""
  642. bl_idname = "mantis.link_armature_node_add_target"
  643. bl_label = "Add Target"
  644. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  645. @classmethod
  646. def poll(cls, context):
  647. return hasattr(context, 'node')
  648. def execute(self, context): # unicode for 'a'
  649. num_targets = len( list(context.node.inputs)[6:])//2
  650. context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
  651. context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
  652. return {'FINISHED'}
  653. class LinkArmatureRemoveTargetInput(bpy.types.Operator):
  654. """Remove a DriverVariable input from the active Driver node"""
  655. bl_idname = "mantis.link_armature_node_remove_target"
  656. bl_label = "Remove Target"
  657. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  658. @classmethod
  659. def poll(cls, context):
  660. return hasattr(context, 'node')
  661. def execute(self, context):
  662. n = context.node
  663. n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
  664. return {'FINISHED'}
  665. class CollectionAddNewOutput(bpy.types.Operator):
  666. """Add a new Collection output to the Driver node"""
  667. bl_idname = "mantis.collection_add_new"
  668. bl_label = "+ Child"
  669. bl_options = {'REGISTER', 'UNDO'}
  670. collection_name : bpy.props.StringProperty(default='Collection')
  671. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  672. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  673. socket_invoked : bpy.props.StringProperty(options ={'HIDDEN'}) # set by caller
  674. @classmethod
  675. def poll(cls, context):
  676. return True #(hasattr(context, 'active_node') )
  677. # DUPLICATED CODE HERE, DUMMY
  678. def invoke(self, context, event):
  679. self.tree_invoked = context.node.id_data.name
  680. self.node_invoked = context.node.name
  681. t = context.node.id_data
  682. t.nodes.active = context.node
  683. context.node.select = True
  684. wm = context.window_manager
  685. return wm.invoke_props_dialog(self)
  686. def execute(self, context):
  687. if not self.collection_name:
  688. return {'CANCELLED'}
  689. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  690. # we need to know which socket it is called from...
  691. s = None
  692. for socket in n.outputs:
  693. if socket.identifier == self.socket_invoked:
  694. s=socket; break
  695. parent_path = ''
  696. if s is not None and s.collection_path:
  697. parent_path = s.collection_path + '>'
  698. outer_dict = n.read_declarations_from_json()
  699. current_data = outer_dict
  700. if parent_path:
  701. for name_elem in parent_path.split('>'):
  702. if name_elem == '': continue # HACK around being a bad programmer
  703. current_data = current_data[name_elem]
  704. current_data[self.collection_name] = {}
  705. n.push_declarations_to_json(outer_dict)
  706. n.update_interface()
  707. return {'FINISHED'}
  708. # TODO: should this prune the children, too?
  709. class CollectionRemoveOutput(bpy.types.Operator):
  710. """Remove a Collection output to the Driver node"""
  711. bl_idname = "mantis.collection_remove"
  712. bl_label = "X"
  713. bl_options = {'REGISTER', 'UNDO'}
  714. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  715. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  716. socket_invoked : bpy.props.StringProperty(options ={'HIDDEN'}) # set by caller
  717. @classmethod
  718. def poll(cls, context):
  719. return True #(hasattr(context, 'active_node') )
  720. # DUPLICATED CODE HERE, DUMMY
  721. def invoke(self, context, event):
  722. self.tree_invoked = context.node.id_data.name
  723. self.node_invoked = context.node.name
  724. t = context.node.id_data
  725. t.nodes.active = context.node
  726. context.node.select = True
  727. return self.execute(context)
  728. def execute(self, context):
  729. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  730. s = None
  731. for socket in n.outputs:
  732. if socket.identifier == self.socket_invoked:
  733. s=socket; break
  734. if not s:
  735. return {'CANCELLED'}
  736. parent_path = ''
  737. if s is not None and s.collection_path:
  738. parent_path = s.collection_path + '>'
  739. outer_dict = n.read_declarations_from_json()
  740. current_data = outer_dict
  741. print(parent_path)
  742. if parent_path:
  743. for name_elem in parent_path.split('>')[:-2]: # just skip the last one
  744. print(name_elem)
  745. if name_elem == '': continue # HACK around being a bad programmer
  746. current_data = current_data[name_elem]
  747. del current_data[s.name]
  748. n.push_declarations_to_json(outer_dict)
  749. n.update_interface()
  750. return {'FINISHED'}
  751. def get_socket_enum(operator, context):
  752. valid_types = []; i = -1
  753. from .socket_definitions import TellClasses, MantisSocket
  754. for cls in TellClasses():
  755. if cls.is_valid_interface_type:
  756. valid_types.append( (cls.bl_idname, cls.bl_label, "Socket Type", i := i + 1), )
  757. return valid_types
  758. class B4_4_0_Workaround_NodeTree_Interface_Update(Operator):
  759. """Selects all nodes of same type as active node."""
  760. bl_idname = "mantis.node_tree_interface_update_4_4_0_workaround"
  761. bl_label = "Add Socket to Node Tree"
  762. bl_options = {'REGISTER', 'UNDO'}
  763. socket_name : bpy.props.StringProperty()
  764. output : bpy.props.BoolProperty()
  765. socket_type : bpy.props.EnumProperty(
  766. name="Socket Type",
  767. description="Socket Type",
  768. items=get_socket_enum,
  769. default=0,)
  770. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  771. @classmethod
  772. def poll(cls, context):
  773. return (any_tree_poll(context))
  774. def invoke(self, context, event):
  775. self.tree_invoked = context.active_node.id_data.name
  776. # we use active_node here ^ because we are comparing the active node to the selection.
  777. wm = context.window_manager
  778. return wm.invoke_props_dialog(self)
  779. def execute(self, context):
  780. tree = bpy.data.node_groups[self.tree_invoked]
  781. in_out = 'OUTPUT' if self.output else 'INPUT'
  782. tree.interface.new_socket(self.socket_name, in_out=in_out, socket_type=self.socket_type)
  783. # try to prevent the next execution
  784. # because updating the interface triggers a depsgraph update.
  785. # this doesn't actually work though...TODO
  786. if tree.bl_idname == "MantisTree":
  787. tree.prevent_next_exec=True
  788. return {"FINISHED"}
  789. class ConvertBezierCurveToNURBS(Operator):
  790. """Converts all bezier splines of curve to NURBS."""
  791. bl_idname = "mantis.convert_bezcrv_to_nurbs"
  792. bl_label = "Convert Bezier Curve to NURBS"
  793. bl_options = {'REGISTER', 'UNDO'}
  794. @classmethod
  795. def poll(cls, context):
  796. return (context.active_object is not None) and (context.active_object.type=='CURVE')
  797. def execute(self, context):
  798. from .utilities import nurbs_copy_bez_spline
  799. curve = context.active_object
  800. bez=[]
  801. for spl in curve.data.splines:
  802. if spl.type=='BEZIER':
  803. bez.append(spl)
  804. for bez_spline in bez:
  805. new_spline=nurbs_copy_bez_spline(curve, bez_spline)
  806. curve.data.splines.remove(bez_spline)
  807. return {"FINISHED"}
  808. # this has to be down here for some reason. what a pain
  809. classes = [
  810. MantisGroupNodes,
  811. MantisEditGroup,
  812. MantisNewNodeTree,
  813. InvalidateNodeTree,
  814. ExecuteNodeTree,
  815. # CreateMetaGroup,
  816. QueryNodeSockets,
  817. ForceDisplayUpdate,
  818. CleanUpNodeGraph,
  819. MantisMuteNode,
  820. SelectNodesOfType,
  821. ConnectNodeToInput,
  822. # xForm
  823. AddCustomProperty,
  824. EditCustomProperty,
  825. RemoveCustomProperty,
  826. # EditFCurveNode,
  827. FcurveAddKeyframeInput,
  828. FcurveRemoveKeyframeInput,
  829. # Driver
  830. DriverAddDriverVariableInput,
  831. DriverRemoveDriverVariableInput,
  832. # Armature Link Node
  833. LinkArmatureAddTargetInput,
  834. LinkArmatureRemoveTargetInput,
  835. # managing collections
  836. CollectionAddNewOutput,
  837. CollectionRemoveOutput,
  838. # rigging utilities
  839. ConvertBezierCurveToNURBS,
  840. ]
  841. if (bpy.app.version >= (4, 4, 0)):
  842. classes.append(B4_4_0_Workaround_NodeTree_Interface_Update)
  843. def TellClasses():
  844. return classes