diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index b903313..e3dae24 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -49,7 +49,9 @@ class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node): # Case: sitting modifier (exports from sit position empty) if self.clickable: clickable_bo = bpy.data.objects[self.clickable] - clickable_so = exporter.mgr.find_create_key(plSceneObject, bl=clickable_bo).object + clickable_so = exporter.mgr.find_create_object(plSceneObject, bl=clickable_bo) + # We're deep inside a potentially unrelated node tree... + exporter.export_coordinate_interface(clickable_so, clickable_bo) else: clickable_bo = parent_bo clickable_so = parent_so diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 3bcca4c..c97b3b1 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -56,6 +56,16 @@ class PlasmaNodeBase: return i raise KeyError(key) + def find_input_sockets(self, key, idname=None): + for i in self.inputs: + if i.identifier == key: + if idname is None: + yield i + elif i.links: + node = i.links[0].from_node + if idname == node.bl_idname: + yield i + def find_output(self, key, idname=None): for i in self.outputs: if i.identifier == key: diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index ccb8080..7ea5566 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -19,12 +19,276 @@ from PyHSPlasma import * from .node_core import * from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids +from ..exporter import ExportError -class PlasmaMessageSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): +class PlasmaMessageSocketBase(PlasmaNodeSocketBase): bl_color = (0.004, 0.282, 0.349, 1.0) +class PlasmaMessageSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket): + pass -class PlasmaFootstepSoundMsgNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaMessageNode(PlasmaNodeVariableInput): + @property + def has_callbacks(self): + """This message has callbacks that can be waited on by a Responder""" + return False + + def init(self, context): + self.inputs.new("PlasmaMessageSocket", "Sender", "sender") + + def update(self): + self.ensure_sockets("PlasmaMessageSocket", "Sender", "sender") + + +class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): + bl_category = "MSG" + bl_idname = "PlasmaAnimCmdMsgNode" + bl_label = "Animation Command" + bl_width_default = 190 + + anim_type = EnumProperty(name="Type", + description="Animation type to affect", + items=[("OBJECT", "Object", "Mesh Action"), + ("TEXTURE", "Texture", "Texture Action")], + default="OBJECT") + object_name = StringProperty(name="Object", + description="Target object name") + material_name = StringProperty(name="Material", + description="Target material name") + texture_name = StringProperty(name="Texture", + description="Target texture slot name") + + go_to = EnumProperty(name="Go To", + description="Where should the animation start?", + items=[("kGoToBegin", "Beginning", "The beginning"), + ("kGoToLoopBegin", "Loop Beginning", "The beginning of the active loop"), + ("CURRENT", "(Don't Change)", "The current position"), + ("kGoToEnd", "Ending", "The end"), + ("kGoToLoopEnd", "Loop Ending", "The end of the active loop")], + default="CURRENT") + action = EnumProperty(name="Action", + description="What do you want the animation to do?", + items=[("kContinue", "Play", "Plays the animation"), + ("kPlayToPercent", "Play to Percent", "Plays the animation until a given percent is complete"), + ("kPlayToTime", "Play to Frame", "Plays the animation up to a given frame number"), + ("kStop", "Stop", "Stops the animation",), + ("kToggleState", "Toggle", "Toggles between Play and Stop"), + ("CURRENT", "(Don't Change)", "Don't change the animation's playing state")], + default="CURRENT") + play_direction = EnumProperty(name="Direction", + description="Which direction do you want to play from?", + items=[("kSetForwards", "Forward", "Play forwards"), + ("kSetBackwards", "Backwards", "Play backwards"), + ("CURRENT", "(Don't Change)", "Don't change the play direction")], + default="CURRENT") + play_to_percent = IntProperty(name="Play To", + description="Percentage at which to stop the animation", + subtype="PERCENTAGE", + min=0, max=100, default=50) + play_to_frame = IntProperty(name="Play To", + description="Frame at which to stop the animation", + min=0) + + def _set_loop_name(self, context): + """Updates loop_begin and loop_end when the loop name is changed""" + pass + + looping = EnumProperty(name="Looping", + description="Is the animation looping?", + items=[("kSetLooping", "Yes", "The animation is looping",), + ("CURRENT", "(Don't Change)", "Don't change the loop status"), + ("kSetUnLooping", "No", "The animation is NOT looping")], + default="CURRENT") + loop_name = StringProperty(name="Active Loop", + description="Name of the active loop", + update=_set_loop_name) + loop_begin = IntProperty(name="Loop Begin", + description="Frame number at which the loop begins", + min=0) + loop_end = IntProperty(name="Loop End", + description="Frame number at which the loop ends", + min=0) + + event = EnumProperty(name="Callback", + description="Event upon which to callback the Responder", + items=[("kEnd", "End", "When the action ends"), + ("NONE", "(None)", "Don't notify the Responder at all"), + ("kStop", "Stop", "When the action is stopped by a message")], + default="kEnd") + + def draw_buttons(self, context, layout): + layout.prop(self, "anim_type") + if self.anim_type == "OBJECT": + layout.prop_search(self, "object_name", bpy.data, "objects") + else: + layout.prop_search(self, "material_name", bpy.data, "materials") + material = bpy.data.materials.get(self.material_name, None) + if material is not None: + layout.prop_search(self, "texture_name", material, "texture_slots") + + layout.prop(self, "go_to") + layout.prop(self, "action") + layout.prop(self, "play_direction") + if self.action == "kPlayToPercent": + layout.prop(self, "play_to_percent") + elif self.action == "kPlayToTime": + layout.prop(self, "play_to_frame") + + layout.prop(self, "looping") + col = layout.column() + col.enabled = self.looping != "CURRENT" + if self.anim_type != "OBJECT": + loops = None + else: + obj = bpy.data.objects.get(self.object_name, None) + loops = None if obj is None else obj.plasma_modifiers.animation_loop + if loops is not None and loops.enabled: + layout.prop_search(self, "loop_name", loops, "loops", icon="PMARKER_ACT") + else: + layout.prop(self, "loop_begin") + layout.prop(self, "loop_end") + + layout.prop(self, "event") + + def convert_callback_message(self, exporter, tree, so, msg, target, wait): + cb = plEventCallbackMsg() + cb.addReceiver(target) + cb.event = globals()[self.event] + cb.user = wait + msg.addCallback(cb) + + def convert_message(self, exporter, tree, so): + msg = plAnimCmdMsg() + + # We're either sending this off to an AGMasterMod or a LayerAnim + error = ExportError("Node '{}' in '{}' specifies an invalid animation".format(self.name, tree.name)) + if self.anim_type == "OBJECT": + obj = bpy.data.objects.get(self.object_name, None) + if obj is None: + raise error + anim = obj.plasma_modifiers.animation + if not anim.enabled: + raise error + target = exporter.mgr.find_create_key(plAGMasterMod, bl=obj, name=anim.display_name) + else: + material = bpy.data.materials.get(self.material_name, None) + if material is None: + raise error + tex_slot = material.texture_slots.get(self.texture_name, None) + if tex_slot is None: + raise error + name = "{}_{}_LayerAnim".format(self.material_name, self.texture_name) + target = exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so) + if target is None: + raise error + msg.addReceiver(target) + + # Check the enum properties to see what commands we need to add + for prop in (self.go_to, self.action, self.play_direction, self.looping): + cmd = getattr(plAnimCmdMsg, prop, None) + if cmd is not None: + msg.setCmd(cmd, True) + + # Easier part starts here??? + fps = bpy.context.scene.render.fps + if self.action == "kPlayToPercent": + msg.time = self.play_to_percent + elif self.action == "kPlayToTime": + msg.time = self.play_to_frame / fps + + # Implicit s better than explicit, I guess... + if self.loop_begin != self.loop_end: + # NOTE: loop name is not used in the engine AFAICT + msg.setCmd(plAnimCmdMsg.kSetLoopBegin, True) + msg.setCmd(plAnimCmdMsg.kSetLoopEnd, True) + msg.loopBegin = self.loop_begin / fps + msg.loopEnd = self.loop_end / fps + + # Whew, this was crazy + return msg + + @property + def has_callbacks(self): + return self.event != "NONE" + + +class PlasmaOneShotMsgNode(PlasmaMessageNode, bpy.types.Node): + bl_category = "MSG" + bl_idname = "PlasmaOneShotMsgNode" + bl_label = "One Shot" + bl_width_default = 210 + + pos = StringProperty(name="Position", + description="Object defining the OneShot position") + seek = EnumProperty(name="Seek", + description="How the avatar should approach the OneShot position", + items=[("SMART", "Smart Seek", "Let the engine figure out the best path"), + ("DUMB", "Seek", "Shuffle to the OneShot position"), + ("NONE", "Warp", "Warp the avatar to the OneShot position")], + default="SMART") + + animation = StringProperty(name="Animation", + description="Name of the animation the avatar should execute") + marker = StringProperty(name="Marker", + description="Name of the marker specifying when to notify the Responder") + drivable = BoolProperty(name="Drivable", + description="Player retains control of the avatar during the OneShot", + default=False) + reversable = BoolProperty(name="Reversable", + description="Player can reverse the OneShot", + default=False) + + def convert_callback_message(self, exporter, tree, so, msg, target, wait): + msg.addCallback(self.marker, target, wait) + + def convert_message(self, exporter, tree, so): + msg = plOneShotMsg() + msg.addReceiver(self.get_key(exporter, tree, so)) + return msg + + def draw_buttons(self, context, layout): + layout.prop(self, "animation", text="Anim") + layout.prop(self, "marker") + row = layout.row() + row.prop(self, "drivable") + row.prop(self, "reversable") + layout.prop_search(self, "pos", bpy.data, "objects", icon="EMPTY_DATA") + layout.prop(self, "seek") + + def export(self, exporter, tree, bo, so): + oneshotmod = self.get_key(exporter, tree, so).object + oneshotmod.animName = self.animation + oneshotmod.drivable = self.drivable + oneshotmod.reversable = self.reversable + oneshotmod.smartSeek = self.seek == "SMART" + oneshotmod.noSeek = self.seek == "NONE" + oneshotmod.seekDuration = 1.0 + + def get_key(self, exporter, tree, so): + name = self.create_key_name(tree) + if self.pos: + bo = bpy.data.objects.get(self.pos, None) + if bo is None: + raise ExportError("Node '{}' in '{}' specifies an invalid Position Empty".format(self.name, tree.name)) + pos_so = exporter.mgr.find_create_object(plSceneObject, bl=bo) + return exporter.mgr.find_create_key(plOneShotMod, name=name, so=pos_so) + else: + return exporter.mgr.find_create_key(plOneShotMod, name=name, so=so) + + @property + def has_callbacks(self): + return bool(self.marker) + + +class PlasmaOneShotCallbackSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket): + marker = StringProperty(name="Marker", + description="Marker specifying the time at which to send a callback to this Responder") + + def draw(self, context, layout, node, text): + layout.prop(self, "marker") + + +class PlasmaFootstepSoundMsgNode(PlasmaMessageNode, bpy.types.Node): bl_category = "MSG" bl_idname = "PlasmaFootstepSoundMsgNode" bl_label = "Footstep Sound" @@ -40,7 +304,7 @@ class PlasmaFootstepSoundMsgNode(PlasmaNodeBase, bpy.types.Node): def draw_buttons(self, context, layout): layout.prop(self, "surface") - def convert_message(self, exporter): + def convert_message(self, exporter, tree, so): msg = plArmatureEffectStateMsg() msg.surface = footstep_surface_ids[self.surface] return msg diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 9f6e24b..7daa6f2 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -41,8 +41,6 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node): self.outputs.new("PlasmaRespStateSocket", "States", "states") def draw_buttons(self, context, layout): - self.ensure_sockets("PlasmaConditionSocket", "Condition", "condition") - layout.prop(self, "detect_trigger") layout.prop(self, "detect_untrigger") layout.prop(self, "no_ff_sounds") @@ -71,10 +69,10 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node): def get_state(self, node): for idx, (theNode, theState) in enumerate(self.states): if theNode == node: - return (idx, theState) + return (idx, theState, True) state = plResponderModifier_State() self.states.append((node, state)) - return (len(self.states) - 1, state) + return (len(self.states) - 1, state, False) def save(self): resp = self.responder @@ -85,9 +83,12 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node): # Convert the Responder states stateMgr = ResponderStateMgr(self, responder) for stateNode in self.find_outputs("states", "PlasmaResponderStateNode"): - stateNode.convert_state(exporter, stateMgr) + stateNode.convert_state(exporter, tree, so, stateMgr) stateMgr.save() + def update(self): + self.ensure_sockets("PlasmaConditionSocket", "Condition", "condition") + class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node): bl_category = "LOGIC" @@ -104,15 +105,10 @@ class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node): self.outputs.new("PlasmaRespStateSocket", "Trigger", "gotostate").link_limit = 1 def draw_buttons(self, context, layout): - # This actually draws nothing, but it makes sure we have at least one empty input slot - # We need this because it's possible that multiple OTHER states can call us - self.ensure_sockets("PlasmaRespStateSocket", "Condition", "condition") - - # Now draw a prop layout.prop(self, "default_state") - def convert_state(self, exporter, stateMgr): - idx, state = stateMgr.get_state(self) + def convert_state(self, exporter, tree, so, stateMgr): + idx, state, converted = stateMgr.get_state(self) # No sanity checking here. Hopefully nothing crazy has happened in the UI. if self.default_state: @@ -123,30 +119,25 @@ class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node): if toStateNode is None: state.switchToState = idx else: - toIdx, toState = stateMgr.get_state(toStateNode) + toIdx, toState, converted = stateMgr.get_state(toStateNode) state.switchToState = toIdx + if not converted: + toStateNode.convert_state(exporter, tree, so, stateMgr) class CommandMgr: def __init__(self): self.commands = [] self.waits = {} - def add_command(self, node): - cmd = type("ResponderCommand", (), {"msg": None, "waitOn": -1}) + def add_command(self, node, waitOn): + cmd = type("ResponderCommand", (), {"msg": None, "waitOn": waitOn}) self.commands.append((node, cmd)) return (len(self.commands) - 1, cmd) - def add_wait(self, parentCmd): - try: - idx = self.commands.index(parentCmd) - except ValueError: - # The parent command didn't export for some reason... Probably no message. - # So, wait on nothing! - return -1 - else: - wait = len(self.waits) - self.waits[wait] = idx - return idx + def add_wait(self, parentIdx): + wait = len(self.waits) + self.waits[wait] = parentIdx + return wait def save(self, state): for node, cmd in self.commands: @@ -161,9 +152,20 @@ class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node): for i in self.find_outputs("cmds", "PlasmaResponderCommandNode"): # slight optimization--commands attached to states can't wait on other commands # namely because it's impossible to wait on a command that doesn't exist... - i.convert_command(exporter, stateMgr.responder, commands, True) + i.convert_command(exporter, tree, so, stateMgr.responder, commands) commands.save(state) + def update(self): + # This actually draws nothing, but it makes sure we have at least one empty input slot + # We need this because it's possible that multiple OTHER states can call us + self.ensure_sockets("PlasmaRespStateSocket", "Condition", "condition") + + # Check to see if we're the default state + if not self.default_state: + inputs = list(self.find_input_sockets("condition", "PlasmaResponderNode")) + if len(inputs) == 1: + self.default_state = True + class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): bl_color = (0.388, 0.78, 0.388, 1.0) @@ -179,36 +181,31 @@ class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node): self.outputs.new("PlasmaMessageSocket", "Message", "msg") self.outputs.new("PlasmaRespCommandSocket", "Trigger", "trigger") - def convert_command(self, exporter, responder, commandMgr, forceNoWait=False): + def convert_command(self, exporter, tree, so, responder, commandMgr, waitOn=-1): # If this command has no message, there is no need to export it... msgNode = self.find_output("msg") if msgNode is not None: - idx, command = commandMgr.add_command(self) - - # If the thingthatdoneit is another command, we need to register a wait. - # We could hack and assume the parent is idx-1, but that won't work if the parent has many - # child commands. Le whoops! - if not forceNoWait: - parentCmd = self.find_input("whodoneit", "PlasmaResponderCommandNode") - if parentCmd is not None: - command.waitOn = commandMgr.add_wait(parentCmd) + idx, command = commandMgr.add_command(self, waitOn) # Finally, convert our message... - msg = msgNode.convert_message(exporter) + msg = msgNode.convert_message(exporter, tree, so) self._finalize_message(exporter, responder, msg) # If we have child commands, we need to make sure that we support chaining this message as a callback # If not, we'll export our children and tell them to not actually wait on us. haveChildren = self.find_output("trigger", "PlasmaResponderCommandNode") is not None - if haveChildren: - nowait = not self._add_msg_callback(exporter, responder, msg) + if haveChildren and msgNode.has_callbacks: + childWaitOn = commandMgr.add_wait(idx) + msgNode.convert_callback_message(exporter, tree, so, msg, responder.key, childWaitOn) + else: + childWaitOn = -1 command.msg = msg else: - nowait = True + childWaitOn = -1 # Export any child commands for i in self.find_outputs("trigger", "PlasmaResponderCommandNode"): - i.convert_command(exporter, responder, commandMgr, nowait) + i.convert_command(exporter, tree, so, responder, commandMgr, childWaitOn) _bcast_flags = { plArmatureEffectStateMsg: (plMessage.kPropagateToModifiers | plMessage.kNetPropagate), @@ -223,12 +220,6 @@ class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node): msg.BCastFlags = self._bcast_flags[_cls] msg.BCastFlags |= plMessage.kLocalPropagate - def _add_msg_callback(self, exporter, responder, msg): - """Prepares a given message to be a callback to the responder""" - # We do not support callback messages ATM - return False - class PlasmaRespCommandSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): bl_color = (0.451, 0.0, 0.263, 1.0) - diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index ded0414..024f913 100644 --- a/korman/operators/op_export.py +++ b/korman/operators/op_export.py @@ -21,7 +21,7 @@ import pstats from .. import exporter from ..properties.prop_world import PlasmaAge - +from ..properties.modifiers.logic import game_versions class ExportOperator(bpy.types.Operator): """Exports ages for Cyan Worlds' Plasma Engine""" @@ -44,9 +44,7 @@ class ExportOperator(bpy.types.Operator): "version": (EnumProperty, {"name": "Version", "description": "Version of the Plasma Engine to target", "default": "pvPots", # This should be changed when moul is easier to target! - "items": [("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game", 2), - ("pvPots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack", 1), - ("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online game", 0)]}), + "items": game_versions}), } # This wigs out and very bad things happen if it's not directly on the operator... diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 86f3852..cff916e 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -123,9 +123,9 @@ class PlasmaAnimationModifier(PlasmaModifierProperties): # We need both an AGModifier and an AGMasterMod # TODO: grouped animations (eg one door, two objects) - agmod = exporter.mgr.add_object(plAGModifier, so=so, name=self.display_name) + agmod = exporter.mgr.find_create_object(plAGModifier, so=so, name=self.display_name) agmod.channelName = bo.name - agmaster = exporter.mgr.add_object(plAGMasterMod, so=so, name=self.display_name) + agmaster = exporter.mgr.find_create_object(plAGMasterMod, so=so, name=self.display_name) agmaster.addPrivateAnim(atcanim.key) def post_export(self, exporter, bo, so): diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index d622c5f..9ee7dd3 100644 --- a/korman/properties/modifiers/logic.py +++ b/korman/properties/modifiers/logic.py @@ -18,6 +18,28 @@ from bpy.props import * from PyHSPlasma import * from .base import PlasmaModifierProperties +from ...exporter import ExportError + +game_versions = [("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"), + ("pvPots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack"), + ("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online game")] + +class PlasmaVersionedNodeTree(bpy.types.PropertyGroup): + name = StringProperty(name="Name") + version = EnumProperty(name="Version", + description="Plasma versions this node tree exports under", + items=game_versions, + options={"ENUM_FLAG"}, + default=set(list(zip(*game_versions))[0])) + node_tree_name = StringProperty(name="Node Tree", + description="Node Tree to export") + + @property + def node_tree(self): + try: + return bpy.data.node_groups[self.node_tree_name] + except KeyError: + raise ExportError("Node Tree {} does not exist!".format(self.node_tree_name)) class PlasmaAdvancedLogic(PlasmaModifierProperties): pl_id = "advanced_logic" @@ -27,19 +49,25 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties): bl_description = "Plasma Logic Nodes" bl_icon = "NODETREE" - tree_name = StringProperty(name="Node Tree", description="Plasma Logic Nodes") + logic_groups = CollectionProperty(type=PlasmaVersionedNodeTree) + active_group_index = IntProperty(options={"HIDDEN"}) def created(self, obj): self.display_name = "Advanced Logic" def export(self, exporter, bo, so): - tree = bpy.data.node_groups[self.tree_name] - tree.export(exporter, bo, so) + version = exporter.mgr.getVer() + for i in self.logic_groups: + our_versions = [globals()[j] for j in i.version] + if version in our_versions: + i.node_tree.export(exporter, bo, so) @property def requires_actor(self): - tree = bpy.data.node_groups[self.tree_name] - return tree.requires_actor + for i in self.logic_groups: + if i.node_tree.requires_actor: + return True + return False class PlasmaSpawnPoint(PlasmaModifierProperties): diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py index e6cad98..70f53f9 100644 --- a/korman/ui/modifiers/logic.py +++ b/korman/ui/modifiers/logic.py @@ -15,8 +15,31 @@ import bpy +class LogicListUI(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): + layout.prop(item, "name", emboss=False, text="", icon="NODETREE") + def advanced_logic(modifier, layout, context): - layout.prop_search(modifier, "tree_name", bpy.data, "node_groups", icon="NODETREE") + row = layout.row() + row.template_list("LogicListUI", "logic_groups", modifier, "logic_groups", modifier, "active_group_index", + rows=2, maxrows=3) + col = row.column(align=True) + op = col.operator("object.plasma_modifier_collection_add", icon="ZOOMIN", text="") + op.modifier = modifier.pl_id + op.collection = "logic_groups" + op.name_prefix = "Logic" + op.name_prop = "name" + op = col.operator("object.plasma_modifier_collection_remove", icon="ZOOMOUT", text="") + op.modifier = modifier.pl_id + op.collection = "logic_groups" + op.index = modifier.active_group_index + + # Modify the loop points + if modifier.logic_groups: + logic = modifier.logic_groups[modifier.active_group_index] + row = layout.row() + row.prop_menu_enum(logic, "version") + row.prop_search(logic, "node_tree_name", bpy.data, "node_groups", icon="NODETREE", text="") def spawnpoint(modifier, layout, context): layout.label(text="Avatar faces negative Y.")