ops_nodegroup.py 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090
  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. def tag_mantis_node_for_update(context, node):
  20. if any_tree_poll(context):
  21. from .base_definitions import get_signature_from_edited_tree
  22. node_tree = context.space_data.path[0].node_tree
  23. mantis_node = node_tree.parsed_tree.get(
  24. get_signature_from_edited_tree(node, context))
  25. if mantis_node is None:
  26. node_tree.hash = ''
  27. else:
  28. mantis_node.reset_execution_recursive() # tag this as a change.
  29. #########################################################################3
  30. class MantisGroupNodes(Operator):
  31. """Create node-group from selected nodes"""
  32. bl_idname = "mantis.group_nodes"
  33. bl_label = "Group Nodes"
  34. bl_options = {'REGISTER', 'UNDO'}
  35. @classmethod
  36. def poll(cls, context):
  37. return mantis_tree_poll_op(context)
  38. def execute(self, context):
  39. base_tree=context.space_data.path[-1].node_tree
  40. try:
  41. for path_item in context.space_data.path:
  42. path_item.node_tree.is_exporting = True
  43. from .i_o import export_to_json, do_import
  44. from random import random
  45. grp_name = "".join([chr(int(random()*30)+35) for i in range(20)])
  46. trees=[base_tree]
  47. selected_nodes=export_to_json(trees, base_tree=None, write_file=False, only_selected=True)
  48. # this snippet of confusing indirection edits the name of the base tree in the JSON data
  49. selected_nodes[base_tree.name][0]["name"]=grp_name
  50. do_import(selected_nodes, context)
  51. affected_links_in = []
  52. affected_links_out = []
  53. for l in base_tree.links:
  54. if l.from_node.select and not l.to_node.select: affected_links_out.append(l)
  55. if not l.from_node.select and l.to_node.select: affected_links_in.append(l)
  56. delete_me = []
  57. all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
  58. for n in base_tree.nodes:
  59. if n.select:
  60. node_loc = (0,0,0)
  61. if bpy.app.version <= (4, 4):
  62. node_loc = n.location
  63. parent = n.parent
  64. while (parent): # accumulate parent offset
  65. node_loc += parent.location
  66. parent = parent.parent
  67. else: # there is a new location_absolute property in 4.4
  68. node_loc = n.location_absolute
  69. if node_loc.x < all_nodes_bounding_box[0].x:
  70. all_nodes_bounding_box[0].x = node_loc.x
  71. if node_loc.y < all_nodes_bounding_box[0].y:
  72. all_nodes_bounding_box[0].y = node_loc.y
  73. #
  74. if node_loc.x > all_nodes_bounding_box[1].x:
  75. all_nodes_bounding_box[1].x = node_loc.x
  76. if node_loc.y > all_nodes_bounding_box[1].y:
  77. all_nodes_bounding_box[1].y = node_loc.y
  78. delete_me.append(n)
  79. grp_node = base_tree.nodes.new('MantisNodeGroup')
  80. grp_node.node_tree = bpy.data.node_groups[grp_name]
  81. bb_center = all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1],0.5)
  82. for n in grp_node.node_tree.nodes:
  83. n.location -= bb_center
  84. 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))
  85. from .base_definitions import node_group_update
  86. grp_node.is_updating=True
  87. try:
  88. node_group_update(grp_node, force=True)
  89. finally:
  90. grp_node.is_updating=False
  91. # for each node in the JSON
  92. for n in selected_nodes[base_tree.name][2].values():
  93. for socket_collection in ['inputs', 'outputs']:
  94. for s in n[socket_collection].values(): # for each socket in the node
  95. if source := s.get("source"):
  96. prGreen (s["name"], source[0], source[1])
  97. base_tree_node=base_tree.nodes.get(source[0])
  98. if s["is_output"]:
  99. for output in base_tree_node.outputs:
  100. if output.identifier == source[1]:
  101. break
  102. else:
  103. raise RuntimeError(wrapRed("Socket not found when grouping"))
  104. base_tree.links.new(input=output, output=grp_node.inputs[s["name"]])
  105. else:
  106. for s_input in base_tree_node.inputs:
  107. if s_input.identifier == source[1]:
  108. break
  109. else:
  110. raise RuntimeError(wrapRed("Socket not found when grouping"))
  111. base_tree.links.new(input=grp_node.outputs[s["name"]], output=s_input)
  112. for n in delete_me: base_tree.nodes.remove(n)
  113. base_tree.nodes.active = grp_node
  114. finally: # MAKE SURE to turn it back to not exporting
  115. for path_item in context.space_data.path:
  116. path_item.node_tree.is_exporting = False
  117. grp_node.node_tree.name = "Group_Node.000"
  118. return {'FINISHED'}
  119. class MantisEditGroup(Operator):
  120. """Edit the group referenced by the active node (or exit the current node-group)"""
  121. bl_idname = "mantis.edit_group"
  122. bl_label = "Edit Group"
  123. bl_options = {'REGISTER', 'UNDO'}
  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. base_tree = path[0].node_tree
  134. live_update_start_state=base_tree.do_live_update
  135. base_tree.do_live_update = False
  136. base_tree.is_executing = True
  137. try:
  138. if hasattr(node, "node_tree"):
  139. if (node.node_tree):
  140. path.append(node.node_tree, node=node)
  141. path[0].node_tree.display_update(context)
  142. return {"FINISHED"}
  143. elif len(path) > 1:
  144. path.pop()
  145. # get the active node in the current path
  146. active = path[len(path)-1].node_tree.nodes.active
  147. from .base_definitions import node_group_update
  148. active.is_updating = True
  149. try:
  150. node_group_update(active, force = True)
  151. finally:
  152. active.is_updating = False
  153. # call update to force the node group to check if its tree has changed
  154. # now we need to loop through the tree and update all node groups of this type.
  155. from .utilities import get_all_nodes_of_type
  156. for g in get_all_nodes_of_type(base_tree, "MantisNodeGroup") + \
  157. get_all_nodes_of_type(base_tree, "MantisSchemaGroup"):
  158. if g.node_tree == active.node_tree:
  159. g.is_updating = True
  160. active.is_updating = True
  161. try:
  162. node_group_update(g, force = True)
  163. finally:
  164. g.is_updating = False
  165. active.is_updating = False
  166. base_tree.display_update(context)
  167. base_tree.is_executing = True
  168. # base_tree.is_executing = True # because it seems display_update unsets this.
  169. finally:
  170. base_tree.do_live_update = live_update_start_state
  171. base_tree.is_executing = False
  172. # HACK
  173. base_tree.handler_flip = True # HACK
  174. # HACK
  175. # I have no idea why but the operator finishing causes the exeuction handler to fire
  176. # I have no control over this since it happens after the execution returns...
  177. # so I have to do this ridiculous hack with a Boolean flip bit.
  178. return {"FINISHED"}
  179. class MantisNewNodeTree(Operator):
  180. """Add a new Mantis tree."""
  181. bl_idname = "mantis.new_node_tree"
  182. bl_label = "New Node Group"
  183. bl_options = {'REGISTER', 'UNDO'}
  184. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  185. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  186. @classmethod
  187. def poll(cls, context):
  188. return (
  189. mantis_tree_poll_op(context) and \
  190. context.node.bl_idname in ["MantisNodeGroup", "MantisSchemaGroup"]
  191. )
  192. @classmethod
  193. def poll(cls, context):
  194. return True #(hasattr(context, 'active_node') )
  195. def invoke(self, context, event):
  196. self.tree_invoked = context.node.id_data.name
  197. self.node_invoked = context.node.name
  198. return self.execute(context)
  199. def execute(self, context):
  200. node = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  201. if node.bl_idname == "MantisSchemaGroup":
  202. if (node.node_tree):
  203. print('a')
  204. return {"CANCELLED"}
  205. else:
  206. from bpy import data
  207. print('b')
  208. node.node_tree = data.node_groups.new(name='Schema Group', type='SchemaTree')
  209. return {'FINISHED'}
  210. elif node.bl_idname == "MantisNodeGroup":
  211. if (node.node_tree):
  212. print('c')
  213. return {"CANCELLED"}
  214. else:
  215. from bpy import data
  216. print('d')
  217. node.node_tree = data.node_groups.new(name='Mantis Group', type='MantisTree')
  218. return {'FINISHED'}
  219. else:
  220. return {"CANCELLED"}
  221. class InvalidateNodeTree(Operator):
  222. """Invalidates this node tree, forcing it to read all data again."""
  223. bl_idname = "mantis.invalidate_node_tree"
  224. bl_label = "Clear Node Tree Cache"
  225. bl_options = {'REGISTER', 'UNDO'}
  226. @classmethod
  227. def poll(cls, context):
  228. return (mantis_tree_poll_op(context))
  229. def execute(self, context):
  230. print("Clearing Active Node Tree cache")
  231. tree=context.space_data.path[0].node_tree
  232. tree.execution_id=''; tree.hash=''
  233. return {"FINISHED"}
  234. class ExecuteNodeTree(Operator):
  235. """Execute this node tree"""
  236. bl_idname = "mantis.execute_node_tree"
  237. bl_label = "Execute Node Tree"
  238. bl_options = {'REGISTER', 'UNDO'}
  239. @classmethod
  240. def poll(cls, context):
  241. return (mantis_tree_poll_op(context))
  242. def execute(self, context):
  243. from time import time
  244. from .utilities import wrapGreen
  245. tree=context.space_data.path[0].node_tree
  246. import cProfile
  247. from os import environ
  248. start_time = time()
  249. do_profile=False
  250. if environ.get("DOPROFILE"):
  251. do_profile=True
  252. pass_error = True
  253. if environ.get("DOERROR"):
  254. pass_error=False
  255. if do_profile:
  256. import pstats, io
  257. from pstats import SortKey
  258. with cProfile.Profile() as pr:
  259. tree.update_tree(context, error_popups = pass_error)
  260. tree.execute_tree(context, error_popups = pass_error)
  261. # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
  262. s = io.StringIO()
  263. sortby = SortKey.TIME
  264. # sortby = SortKey.CUMULATIVE
  265. ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
  266. ps.print_stats(20) # print the top 20
  267. print(s.getvalue())
  268. else:
  269. tree.update_tree(context, error_popups = pass_error)
  270. tree.execute_tree(context, error_popups = pass_error)
  271. prGreen("Finished executing tree in %f seconds" % (time() - start_time))
  272. return {"FINISHED"}
  273. class SelectNodesOfType(Operator):
  274. """Selects all nodes of same type as active node."""
  275. bl_idname = "mantis.select_nodes_of_type"
  276. bl_label = "Select Nodes of Same Type as Active"
  277. bl_options = {'REGISTER', 'UNDO'}
  278. @classmethod
  279. def poll(cls, context):
  280. return (any_tree_poll(context))
  281. def execute(self, context):
  282. active_node = context.active_node
  283. tree = active_node.id_data
  284. if not hasattr(active_node, "node_tree"):
  285. for node in tree.nodes:
  286. node.select = (active_node.bl_idname == node.bl_idname)
  287. else:
  288. for node in tree.nodes:
  289. node.select = (active_node.bl_idname == node.bl_idname) and (active_node.node_tree == node.node_tree)
  290. return {"FINISHED"}
  291. def get_parent_tree_interface_enum(operator, context):
  292. ret = []; i = -1
  293. tree = bpy.data.node_groups[operator.tree_invoked]
  294. for sock in tree.interface.items_tree:
  295. if sock.item_type == 'PANEL': continue
  296. if sock.in_out == "OUTPUT": continue
  297. ret.append( (sock.identifier, sock.name, "Socket from Node Group Input", i := i + 1), )
  298. return ret
  299. def get_node_inputs_enum(operator, context):
  300. ret = []; i = -1
  301. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  302. for inp in n.inputs:
  303. ret.append( (inp.identifier, inp.name, "Socket of node to connect to.", i := i + 1), )
  304. return ret
  305. class ConnectNodeToInput(Operator):
  306. """Connects a Node Group Input socket to specified socket of active node and all selected same-type nodes."""
  307. bl_idname = "mantis.connect_nodes_to_input"
  308. bl_label = "Connect Socket to Input for Selected Nodes"
  309. bl_options = {'REGISTER', 'UNDO'}
  310. group_output : bpy.props.EnumProperty(
  311. items=get_parent_tree_interface_enum,
  312. name="Node Group Input Socket",
  313. description="Select which socket from the Node Group Input to connect to this node",)
  314. node_input : bpy.props.EnumProperty(
  315. items=get_node_inputs_enum,
  316. name="Node Input Socket",
  317. description="Select which of this node's sockets to recieve the connection",)
  318. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  319. world_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  320. material_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  321. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  322. @classmethod
  323. def poll(cls, context):
  324. is_tree = (any_tree_poll(context))
  325. is_material = False
  326. if hasattr(context, 'space_data') and context.space_data.type == 'NODE_EDITOR':
  327. is_material = context.space_data.node_tree.bl_idname == 'ShaderNodeTree'
  328. return is_tree and not is_material # doesn't work for Material right now TODO
  329. def invoke(self, context, event):
  330. self.material_invoked=''; self.world_invoked=''
  331. self.tree_invoked = context.active_node.id_data.name
  332. if context.space_data.node_tree.bl_idname == 'ShaderNodeTree':
  333. if context.space_data.shader_type == 'WORLD':
  334. self.world_invoked = context.space_data.id.name
  335. elif context.space_data.shader_type == 'OBJECT':
  336. self.material_invoked = context.space_data.id.name
  337. else:
  338. return {'CANCELLED'} # what is the LINESTYLE menu? TODO
  339. self.node_invoked = context.active_node.name
  340. # we use active_node here ^ because we are comparing the active node to the selection.
  341. wm = context.window_manager
  342. return wm.invoke_props_dialog(self)
  343. def execute(self, context):
  344. if context.space_data.node_tree.bl_idname == 'ShaderNodeTree':
  345. if context.space_data.shader_type == 'WORLD':
  346. t = bpy.data.worlds[self.material_invoked].node_tree
  347. elif context.space_data.shader_type == 'OBJECT':
  348. t = bpy.data.materials[self.material_invoked].node_tree
  349. else:
  350. t = bpy.data.node_groups.get(self.tree_invoked)
  351. if hasattr(t, "is_executing"): # for Mantis trees, but this function should just work anywhere.
  352. t.is_executing = True
  353. n = t.nodes[self.node_invoked]
  354. for node in t.nodes:
  355. if n.bl_idname == node.bl_idname and node.select:
  356. # the bl_idname is the same so they both have node_tree
  357. if hasattr(n, "node_tree") and n.node_tree != node.node_tree: continue
  358. # TODO: maybe I should try and find a nearby input node and reuse it
  359. # doing these identifier lookups again and again is slow, whatever. faster than doing it by hand
  360. for connect_to_me in node.inputs:
  361. if connect_to_me.identifier == self.node_input: break
  362. if connect_to_me.is_linked: connect_to_me = None
  363. if connect_to_me: # only make the node if the socket is there and free
  364. inp = t.nodes.new("NodeGroupInput")
  365. connect_me = None
  366. for s in inp.outputs:
  367. if s.identifier != self.group_output: s.hide = True
  368. else: connect_me = s
  369. inp.location = node.location
  370. inp.location.x-=200
  371. if connect_me is None:
  372. continue # I don't know how this happens.
  373. # TODO: this bit doesn't work in shader trees for some reason, fix it
  374. t.links.new(input=connect_me, output=connect_to_me)
  375. if hasattr(t, "is_executing"):
  376. t.is_executing = False
  377. return {"FINISHED"}
  378. class QueryNodeSockets(Operator):
  379. """Utility Operator for querying the data in a socket"""
  380. bl_idname = "mantis.query_sockets"
  381. bl_label = "Query Node Sockets"
  382. bl_options = {'REGISTER', 'UNDO'}
  383. @classmethod
  384. def poll(cls, context):
  385. return (mantis_tree_poll_op(context))
  386. def execute(self, context):
  387. active_node = context.active_node
  388. tree = active_node.id_data
  389. for node in tree.nodes:
  390. if not node.select: continue
  391. return {"FINISHED"}
  392. class FoceUpdateGroup(Operator):
  393. """Developer Tool to force a group update"""
  394. bl_idname = "mantis.force_update_group"
  395. bl_label = "Force Mantis Group Update"
  396. bl_options = {'REGISTER', 'UNDO'}
  397. @classmethod
  398. def poll(cls, context):
  399. return (mantis_tree_poll_op(context))
  400. def execute(self, context):
  401. active_node = context.active_node
  402. from .base_definitions import node_group_update
  403. try:
  404. active_node.is_updating = True
  405. node_group_update(active_node, force=True)
  406. finally:
  407. active_node.is_updating = False
  408. return {"FINISHED"}
  409. class ForceDisplayUpdate(Operator):
  410. """Utility Operator for querying the data in a socket"""
  411. bl_idname = "mantis.force_display_update"
  412. bl_label = "Force Mantis Display Update"
  413. bl_options = {'REGISTER', 'UNDO'}
  414. @classmethod
  415. def poll(cls, context):
  416. return (mantis_tree_poll_op(context))
  417. def execute(self, context):
  418. base_tree = bpy.context.space_data.path[0].node_tree
  419. base_tree.display_update(context)
  420. return {"FINISHED"}
  421. class CleanUpNodeGraph(bpy.types.Operator):
  422. """Clean Up Node Graph"""
  423. bl_idname = "mantis.nodes_cleanup"
  424. bl_label = "Clean Up Node Graph"
  425. bl_options = {'REGISTER', 'UNDO'}
  426. # num_iterations=bpy.props.IntProperty(default=8)
  427. @classmethod
  428. def poll(cls, context):
  429. return hasattr(context, 'active_node')
  430. def execute(self, context):
  431. base_tree=context.space_data.path[-1].node_tree
  432. from .utilities import SugiyamaGraph
  433. SugiyamaGraph(base_tree, 12)
  434. return {'FINISHED'}
  435. class MantisMuteNode(Operator):
  436. """Mantis Test Operator"""
  437. bl_idname = "mantis.mute_node"
  438. bl_label = "Mute Node"
  439. bl_options = {'REGISTER', 'UNDO'}
  440. @classmethod
  441. def poll(cls, context):
  442. return (mantis_tree_poll_op(context))
  443. def execute(self, context):
  444. path = context.space_data.path
  445. node = path[len(path)-1].node_tree.nodes.active
  446. node.mute = not node.mute
  447. # There should only be one of these
  448. if (enable := node.inputs.get("Enable")):
  449. # annoyingly, 'mute' and 'enable' are opposites
  450. enable.default_value = not node.mute
  451. # this one is for Deformers
  452. elif (enable := node.inputs.get("Enable in Viewport")):
  453. # annoyingly, 'mute' and 'enable' are opposites
  454. enable.default_value = not node.mute
  455. elif (hide := node.inputs.get("Hide")):
  456. hide.default_value = node.mute
  457. elif (hide := node.inputs.get("Hide in Viewport")):
  458. hide.default_value = node.mute
  459. return {"FINISHED"}
  460. ePropertyType =(
  461. ('BOOL' , "Boolean", "Boolean", 0),
  462. ('INT' , "Integer", "Integer", 1),
  463. ('FLOAT' , "Float" , "Float" , 2),
  464. ('VECTOR', "Vector" , "Vector" , 3),
  465. ('STRING', "String" , "String" , 4),
  466. #('ENUM' , "Enum" , "Enum" , 5),
  467. )
  468. from .base_definitions import xFormNode
  469. class AddCustomProperty(bpy.types.Operator):
  470. """Add Custom Property to xForm Node"""
  471. bl_idname = "mantis.add_custom_property"
  472. bl_label = "Add Custom Property"
  473. bl_options = {'REGISTER', 'UNDO'}
  474. prop_type : bpy.props.EnumProperty(
  475. items=ePropertyType,
  476. name="New Property Type",
  477. description="Type of data for new Property",
  478. default = 'BOOL',)
  479. prop_name : bpy.props.StringProperty(default='Prop')
  480. min:bpy.props.FloatProperty(default = 0)
  481. max:bpy.props.FloatProperty(default = 1)
  482. soft_min:bpy.props.FloatProperty(default = 0)
  483. soft_max:bpy.props.FloatProperty(default = 1)
  484. description:bpy.props.StringProperty(default = "")
  485. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  486. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  487. @classmethod
  488. def poll(cls, context):
  489. return True #( hasattr(context, 'node') )
  490. def invoke(self, context, event):
  491. self.tree_invoked = context.node.id_data.name
  492. self.node_invoked = context.node.name
  493. wm = context.window_manager
  494. return wm.invoke_props_dialog(self)
  495. def execute(self, context):
  496. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  497. # For whatever reason, context.node doesn't exist anymore
  498. # (probably because I use a window to execute)
  499. # so as a sort of dumb workaround I am saving it to a hidden
  500. # property of the operator... it works but Blender complains.
  501. socktype = ''
  502. if not (self.prop_name):
  503. self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")
  504. return {'CANCELLED'}
  505. if self.prop_type == 'BOOL':
  506. socktype = 'ParameterBoolSocket'
  507. if self.prop_type == 'INT':
  508. socktype = 'ParameterIntSocket'
  509. if self.prop_type == 'FLOAT':
  510. socktype = 'ParameterFloatSocket'
  511. if self.prop_type == 'VECTOR':
  512. socktype = 'ParameterVectorSocket'
  513. if self.prop_type == 'STRING':
  514. socktype = 'ParameterStringSocket'
  515. #if self.prop_type == 'ENUM':
  516. # sock_type = 'ParameterStringSocket'
  517. if (s := n.inputs.get(self.prop_name)):
  518. try:
  519. number = int(self.prop_name[-3:])
  520. # see if it has a number
  521. number+=1
  522. self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
  523. except ValueError:
  524. self.prop_name+='.001'
  525. # WRONG # HACK # TODO # BUG #
  526. new_prop = n.inputs.new( socktype, self.prop_name)
  527. if self.prop_type in ['INT','FLOAT']:
  528. new_prop.min = self.min
  529. new_prop.max = self.max
  530. new_prop.soft_min = self.soft_min
  531. new_prop.soft_max = self.soft_max
  532. new_prop.description = self.description
  533. # now do the output
  534. n.outputs.new( socktype, self.prop_name)
  535. tag_mantis_node_for_update(context, n)
  536. return {'FINISHED'}
  537. def main_get_existing_custom_properties(operator, context):
  538. ret = []; i = -1
  539. n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
  540. for inp in n.inputs:
  541. if 'Parameter' in inp.bl_idname:
  542. ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )
  543. return ret
  544. class EditCustomProperty(bpy.types.Operator):
  545. """Edit Custom Property"""
  546. bl_idname = "mantis.edit_custom_property"
  547. bl_label = "Edit Custom Property"
  548. bl_options = {'REGISTER', 'UNDO'}
  549. def get_existing_custom_properties(self, context):
  550. return main_get_existing_custom_properties(self, context)
  551. prop_edit : bpy.props.EnumProperty(
  552. items=get_existing_custom_properties,
  553. name="Property to Edit?",
  554. description="Select which property to edit",)
  555. prop_type : bpy.props.EnumProperty(
  556. items=ePropertyType,
  557. name="New Property Type",
  558. description="Type of data for new Property",
  559. default = 'BOOL',)
  560. prop_name : bpy.props.StringProperty(default='Prop')
  561. min:bpy.props.FloatProperty(default = 0)
  562. max:bpy.props.FloatProperty(default = 1)
  563. soft_min:bpy.props.FloatProperty(default = 0)
  564. soft_max:bpy.props.FloatProperty(default = 1)
  565. description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
  566. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  567. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  568. @classmethod
  569. def poll(cls, context):
  570. return True #( hasattr(context, 'node') )
  571. def invoke(self, context, event):
  572. self.tree_invoked = context.node.id_data.name
  573. self.node_invoked = context.node.name
  574. wm = context.window_manager
  575. return wm.invoke_props_dialog(self)
  576. def execute(self, context):
  577. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  578. prop = n.inputs.get( self.prop_edit )
  579. if prop:
  580. prop.name = self.prop_name
  581. if (s := n.inputs.get(self.prop_edit)):
  582. if self.prop_type in ['INT','FLOAT']:
  583. prop.min = self.min
  584. prop.max = self.max
  585. prop.soft_min = self.soft_min
  586. prop.soft_max = self.soft_max
  587. prop.description = self.description
  588. tag_mantis_node_for_update(context, n)
  589. return {'FINISHED'}
  590. else:
  591. self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")
  592. class RemoveCustomProperty(bpy.types.Operator):
  593. """Remove a Custom Property from an xForm Node"""
  594. bl_idname = "mantis.remove_custom_property"
  595. bl_label = "Remove Custom Property"
  596. bl_options = {'REGISTER', 'UNDO'}
  597. def get_existing_custom_properties(self, context):
  598. return main_get_existing_custom_properties(self, context)
  599. prop_remove : bpy.props.EnumProperty(
  600. items=get_existing_custom_properties,
  601. name="Property to remove?",
  602. description="Select which property to remove",)
  603. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  604. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  605. @classmethod
  606. def poll(cls, context):
  607. return True #(hasattr(context, 'active_node') )
  608. def invoke(self, context, event):
  609. self.tree_invoked = context.node.id_data.name
  610. self.node_invoked = context.node.name
  611. t = context.node.id_data
  612. # HACK the props dialog makes this necesary
  613. # because context.node only exists during the event that
  614. # was created by clicking on the node.
  615. t.nodes.active = context.node # HACK
  616. context.node.select = True # HACK
  617. # I need this bc of the callback for the enum property.
  618. # for whatever reason I can't use tree_invoked there
  619. wm = context.window_manager
  620. return wm.invoke_props_dialog(self)
  621. def execute(self, context):
  622. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  623. # For whatever reason, context.node doesn't exist anymore
  624. # (probably because I use a window to execute)
  625. # so as a sort of dumb workaround I am saving it to a hidden
  626. # property of the operator... it works.
  627. for i, inp in enumerate(n.inputs):
  628. if inp.identifier == self.prop_remove:
  629. break
  630. else:
  631. self.report({'ERROR'}, "Input not found")
  632. return {'CANCELLED'}
  633. # it's possible that the output property's identifier isn't the
  634. # exact same... but I don' care. Shouldn't ever happen. TODO
  635. for j, out in enumerate(n.outputs):
  636. if out.identifier == self.prop_remove:
  637. break
  638. else:
  639. self.report({'ERROR'}, "Output not found")
  640. raise RuntimeError("This should not happen!")
  641. n.inputs.remove ( n.inputs [i] )
  642. n.outputs.remove( n.outputs[j] )
  643. tag_mantis_node_for_update(context, n)
  644. return {'FINISHED'}
  645. # SIMPLE node operators...
  646. # May rewrite these in a more generic way later
  647. class FcurveAddKeyframeInput(bpy.types.Operator):
  648. """Add a keyframe input to the fCurve node"""
  649. bl_idname = "mantis.fcurve_node_add_kf"
  650. bl_label = "Add Keyframe"
  651. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  652. @classmethod
  653. def poll(cls, context):
  654. return (hasattr(context, 'active_node') )
  655. def execute(self, context):
  656. num_keys = len( context.node.inputs)-1
  657. context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
  658. return {'FINISHED'}
  659. class FcurveRemoveKeyframeInput(bpy.types.Operator):
  660. """Remove a keyframe input from the fCurve node"""
  661. bl_idname = "mantis.fcurve_node_remove_kf"
  662. bl_label = "Remove Keyframe"
  663. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  664. @classmethod
  665. def poll(cls, context):
  666. return (hasattr(context, 'active_node') )
  667. def execute(self, context):
  668. n = context.node
  669. n.inputs.remove(n.inputs[-1])
  670. return {'FINISHED'}
  671. class DriverAddDriverVariableInput(bpy.types.Operator):
  672. """Add a Driver Variable input to the Driver node"""
  673. bl_idname = "mantis.driver_node_add_variable"
  674. bl_label = "Add Driver Variable"
  675. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  676. @classmethod
  677. def poll(cls, context):
  678. return (hasattr(context, 'active_node') )
  679. def execute(self, context): # unicode for 'a'
  680. i = len (context.node.inputs) - 2 + 96
  681. context.node.inputs.new("DriverVariableSocket", chr(i))
  682. return {'FINISHED'}
  683. class DriverRemoveDriverVariableInput(bpy.types.Operator):
  684. """Remove a DriverVariable input from the active Driver node"""
  685. bl_idname = "mantis.driver_node_remove_variable"
  686. bl_label = "Remove Driver Variable"
  687. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  688. @classmethod
  689. def poll(cls, context):
  690. return (hasattr(context, 'active_node') )
  691. def execute(self, context):
  692. n = context.node
  693. n.inputs.remove(n.inputs[-1])
  694. return {'FINISHED'}
  695. class LinkArmatureAddTargetInput(bpy.types.Operator):
  696. """Add a Driver Variable input to the Driver node"""
  697. bl_idname = "mantis.link_armature_node_add_target"
  698. bl_label = "Add Target"
  699. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  700. @classmethod
  701. def poll(cls, context):
  702. return hasattr(context, 'node')
  703. def execute(self, context): # unicode for 'a'
  704. num_targets = len( list(context.node.inputs)[6:])//2
  705. context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
  706. context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
  707. return {'FINISHED'}
  708. class LinkArmatureRemoveTargetInput(bpy.types.Operator):
  709. """Remove a DriverVariable input from the active Driver node"""
  710. bl_idname = "mantis.link_armature_node_remove_target"
  711. bl_label = "Remove Target"
  712. bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
  713. @classmethod
  714. def poll(cls, context):
  715. return hasattr(context, 'node')
  716. def execute(self, context):
  717. n = context.node
  718. n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
  719. return {'FINISHED'}
  720. class CollectionAddNewOutput(bpy.types.Operator):
  721. """Add a new Collection output to the Driver node"""
  722. bl_idname = "mantis.collection_add_new"
  723. bl_label = "+ Child"
  724. bl_options = {'REGISTER', 'UNDO'}
  725. collection_name : bpy.props.StringProperty(default='Collection')
  726. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  727. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  728. socket_invoked : bpy.props.StringProperty(options ={'HIDDEN'}) # set by caller
  729. @classmethod
  730. def poll(cls, context):
  731. return True #(hasattr(context, 'active_node') )
  732. # DUPLICATED CODE HERE, DUMMY
  733. def invoke(self, context, event):
  734. self.tree_invoked = context.node.id_data.name
  735. self.node_invoked = context.node.name
  736. t = context.node.id_data
  737. t.nodes.active = context.node
  738. context.node.select = True
  739. wm = context.window_manager
  740. return wm.invoke_props_dialog(self)
  741. def execute(self, context):
  742. if not self.collection_name:
  743. return {'CANCELLED'}
  744. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  745. # we need to know which socket it is called from...
  746. s = None
  747. for socket in n.outputs:
  748. if socket.identifier == self.socket_invoked:
  749. s=socket; break
  750. parent_path = ''
  751. if s is not None and s.collection_path:
  752. parent_path = s.collection_path + '>'
  753. outer_dict = n.read_declarations_from_json()
  754. current_data = outer_dict
  755. if parent_path:
  756. for name_elem in parent_path.split('>'):
  757. if name_elem == '': continue # HACK around being a bad programmer
  758. current_data = current_data[name_elem]
  759. current_data[self.collection_name] = {}
  760. n.push_declarations_to_json(outer_dict)
  761. n.update_interface()
  762. return {'FINISHED'}
  763. # TODO: should this prune the children, too?
  764. class CollectionRemoveOutput(bpy.types.Operator):
  765. """Remove a Collection output to the Driver node"""
  766. bl_idname = "mantis.collection_remove"
  767. bl_label = "X"
  768. bl_options = {'REGISTER', 'UNDO'}
  769. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  770. node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  771. socket_invoked : bpy.props.StringProperty(options ={'HIDDEN'}) # set by caller
  772. @classmethod
  773. def poll(cls, context):
  774. return True #(hasattr(context, 'active_node') )
  775. # DUPLICATED CODE HERE, DUMMY
  776. def invoke(self, context, event):
  777. self.tree_invoked = context.node.id_data.name
  778. self.node_invoked = context.node.name
  779. t = context.node.id_data
  780. t.nodes.active = context.node
  781. context.node.select = True
  782. return self.execute(context)
  783. def execute(self, context):
  784. n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
  785. s = None
  786. for socket in n.outputs:
  787. if socket.identifier == self.socket_invoked:
  788. s=socket; break
  789. if not s:
  790. return {'CANCELLED'}
  791. parent_path = ''
  792. if s is not None and s.collection_path:
  793. parent_path = s.collection_path + '>'
  794. outer_dict = n.read_declarations_from_json()
  795. current_data = outer_dict
  796. print(parent_path)
  797. if parent_path:
  798. for name_elem in parent_path.split('>')[:-2]: # just skip the last one
  799. print(name_elem)
  800. if name_elem == '': continue # HACK around being a bad programmer
  801. current_data = current_data[name_elem]
  802. del current_data[s.name]
  803. n.push_declarations_to_json(outer_dict)
  804. n.update_interface()
  805. return {'FINISHED'}
  806. def get_socket_enum(operator, context):
  807. valid_types = []; i = -1
  808. from .socket_definitions import TellClasses, MantisSocket
  809. for cls in TellClasses():
  810. if cls.is_valid_interface_type:
  811. valid_types.append( (cls.bl_idname, cls.bl_label, "Socket Type", i := i + 1), )
  812. return valid_types
  813. class B4_4_0_Workaround_NodeTree_Interface_Update(Operator):
  814. """Selects all nodes of same type as active node."""
  815. bl_idname = "mantis.node_tree_interface_update_4_4_0_workaround"
  816. bl_label = "Add Socket to Node Tree"
  817. bl_options = {'REGISTER', 'UNDO'}
  818. socket_name : bpy.props.StringProperty()
  819. output : bpy.props.BoolProperty()
  820. socket_type : bpy.props.EnumProperty(
  821. name="Socket Type",
  822. description="Socket Type",
  823. items=get_socket_enum,
  824. default=0,)
  825. tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
  826. @classmethod
  827. def poll(cls, context):
  828. return (any_tree_poll(context))
  829. def invoke(self, context, event):
  830. self.tree_invoked = context.active_node.id_data.name
  831. # we use active_node here ^ because we are comparing the active node to the selection.
  832. wm = context.window_manager
  833. return wm.invoke_props_dialog(self)
  834. def execute(self, context):
  835. tree = bpy.data.node_groups[self.tree_invoked]
  836. in_out = 'OUTPUT' if self.output else 'INPUT'
  837. tree.interface.new_socket(self.socket_name, in_out=in_out, socket_type=self.socket_type)
  838. # try to prevent the next execution
  839. # because updating the interface triggers a depsgraph update.
  840. # this doesn't actually work though...TODO
  841. if tree.bl_idname == "MantisTree":
  842. tree.prevent_next_exec=True
  843. return {"FINISHED"}
  844. class ConvertBezierCurveToNURBS(Operator):
  845. """Converts all bezier splines of curve to NURBS."""
  846. bl_idname = "mantis.convert_bezcrv_to_nurbs"
  847. bl_label = "Convert Bezier Curve to NURBS"
  848. bl_options = {'REGISTER', 'UNDO'}
  849. @classmethod
  850. def poll(cls, context):
  851. return (context.active_object is not None) and (context.active_object.type=='CURVE')
  852. def execute(self, context):
  853. from .utilities import nurbs_copy_bez_spline
  854. curve = context.active_object
  855. bez=[]
  856. for spl in curve.data.splines:
  857. if spl.type=='BEZIER':
  858. bez.append(spl)
  859. for bez_spline in bez:
  860. new_spline=nurbs_copy_bez_spline(curve, bez_spline)
  861. curve.data.splines.remove(bez_spline)
  862. return {"FINISHED"}
  863. def get_component_library_items(self, context):
  864. import json
  865. from .preferences import get_bl_addon_object
  866. bl_mantis_addon = get_bl_addon_object()
  867. return_value = [('NONE', 'None', 'None', 'ERROR', 0)]
  868. component_name_and_path={}
  869. if bl_mantis_addon:
  870. widgets_path = bl_mantis_addon.preferences.ComponentsLibraryFolder
  871. import os
  872. for path_root, dirs, files, in os.walk(widgets_path):
  873. for file in files:
  874. if file.endswith('.rig'):
  875. filepath = os.path.join(path_root, file)
  876. with open(filepath, 'r', encoding='utf-8') as f:
  877. json_data = json.load(f)
  878. for tree_name in json_data.keys():
  879. component_name_and_path[tree_name] = os.path.join(path_root, file)
  880. if component_name_and_path.keys():
  881. return_value=[]
  882. for i, (name, path) in enumerate(component_name_and_path.items()):
  883. return_value.append( ((path+"|"+name).upper(), name, path + ': ' + name, 'NODE_TREE', i) )
  884. return return_value
  885. class ImportFromComponentLibrary(Operator):
  886. """Import a Mantis Component from your Component Library."""
  887. bl_idname = "mantis.import_from_component_library"
  888. bl_label = "Import From Component Library"
  889. bl_options = {'REGISTER', 'UNDO'}
  890. default_value : bpy.props.EnumProperty(
  891. items=get_component_library_items,
  892. name="Component",
  893. description="Which Component to Import",
  894. default = 0,
  895. )
  896. @classmethod
  897. def poll(cls, context):
  898. return True # probably not best to do a True for this LOL
  899. def execute(self, context):
  900. return {"FINISHED"}
  901. # this has to be down here for some reason. what a pain
  902. classes = [
  903. MantisGroupNodes,
  904. MantisEditGroup,
  905. MantisNewNodeTree,
  906. InvalidateNodeTree,
  907. ExecuteNodeTree,
  908. # CreateMetaGroup,
  909. QueryNodeSockets,
  910. FoceUpdateGroup,
  911. ForceDisplayUpdate,
  912. CleanUpNodeGraph,
  913. MantisMuteNode,
  914. SelectNodesOfType,
  915. ConnectNodeToInput,
  916. # xForm
  917. AddCustomProperty,
  918. EditCustomProperty,
  919. RemoveCustomProperty,
  920. # EditFCurveNode,
  921. FcurveAddKeyframeInput,
  922. FcurveRemoveKeyframeInput,
  923. # Driver
  924. DriverAddDriverVariableInput,
  925. DriverRemoveDriverVariableInput,
  926. # Armature Link Node
  927. LinkArmatureAddTargetInput,
  928. LinkArmatureRemoveTargetInput,
  929. # managing collections
  930. CollectionAddNewOutput,
  931. CollectionRemoveOutput,
  932. # rigging utilities
  933. ConvertBezierCurveToNURBS,
  934. # component library
  935. ImportFromComponentLibrary,
  936. ]
  937. if (bpy.app.version >= (4, 4, 0)):
  938. classes.append(B4_4_0_Workaround_NodeTree_Interface_Update)
  939. def TellClasses():
  940. return classes