diff --git a/korman/enum_props.py b/korman/enum_props.py index 7dcc2cf..bbda865 100644 --- a/korman/enum_props.py +++ b/korman/enum_props.py @@ -20,6 +20,32 @@ from bpy.props import * from typing import * import warnings +# Workaround for Blender memory management limitation, +# don't change this to a literal in the code! +_ENTIRE_ANIMATION = "(Entire Animation)" + +def _get_object_animation_names(self, object_attr: str) -> Sequence[Tuple[str, str, str]]: + target_object = getattr(self, object_attr) + if target_object is not None: + items = [(anim.animation_name, anim.animation_name, "") + for anim in target_object.plasma_modifiers.animation.subanimations] + else: + items = [(_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")] + + # We always want "(Entire Animation)", if it exists, to be the first item. + entire = items.index((_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")) + if entire not in (-1, 0): + items.pop(entire) + items.insert(0, (_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")) + + return items + +def animation(object_attr: str, **kwargs) -> str: + def get_items(self, context): + return _get_object_animation_names(self, object_attr) + + return EnumProperty(items=get_items, **kwargs) + # These are the kinds of physical bounds Plasma can work with. # This sequence is acceptable in any EnumProperty _bounds_types = ( @@ -105,3 +131,37 @@ def upgrade_bounds(bl, bounds_attr: str) -> None: if bounds_value_curr != bounds_value_new: print(f"Stashing bounds property: [{bl.name}] ({cls.__name__}) {bounds_value_curr} -> {bounds_value_new}") # TEMP setattr(bl, bounds_attr, bounds_value_new) + +def _get_texture_animation_names(self, object_attr: str, material_attr: str, texture_attr: str) -> Sequence[Tuple[str, str, str]]: + target_object = getattr(self, object_attr) + material = getattr(self, material_attr) + texture = getattr(self, texture_attr) + + if texture is not None: + items = [(anim.animation_name, anim.animation_name, "") + for anim in texture.plasma_layer.subanimations] + elif material is not None or target_object is not None: + if material is None: + materials = (i.material for i in target_object.material_slots if i and i.material) + else: + materials = (material,) + layer_props = (i.texture.plasma_layer for mat in materials for i in mat.texture_slots if i and i.texture) + all_anims = frozenset((anim.animation_name for i in layer_props for anim in i.subanimations)) + items = [(i, i, "") for i in all_anims] + else: + items = [(_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")] + + # We always want "(Entire Animation)", if it exists, to be the first item. + entire = items.index((_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")) + if entire not in (-1, 0): + items.pop(entire) + items.insert(0, (_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")) + + return items + +def triprop_animation(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> str: + def get_items(self, context): + return _get_texture_animation_names(self, object_attr, material_attr, texture_attr) + + assert not {"items", "get", "set"} & kwargs.keys(), "You cannot use the `items`, `get`, or `set` keyword arguments" + return EnumProperty(items=get_items, **kwargs) diff --git a/korman/idprops.py b/korman/idprops.py index 40cc474..d593c03 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -145,6 +145,86 @@ def poll_visregion_objects(self, value): def poll_envmap_textures(self, value): return isinstance(value, bpy.types.EnvironmentMapTexture) +def triprop_material(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Material: + user_poll = kwargs.pop("poll", None) + + def poll_proc(self, value: bpy.types.Material) -> bool: + target_object = getattr(self, object_attr) + if target_object is None: + target_object = getattr(self, "id_data", None) + + # Don't filter materials by texture - this would (potentially) result in surprising UX + # in that you would have to clear the texture selection before being able to select + # certain materials. + if target_object is not None: + object_materials = (slot.material for slot in target_object.material_slots if slot and slot.material) + result = value in object_materials + else: + result = True + + # Downstream processing, if any. + if result and user_poll is not None: + result = user_poll(self, value) + return result + + assert not "type" in kwargs + return PointerProperty( + type=bpy.types.Material, + poll=poll_proc, + **kwargs + ) + +def triprop_object(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Texture: + assert not "type" in kwargs + if not "poll" in kwargs: + kwargs["poll"] = poll_drawable_objects + return PointerProperty( + type=bpy.types.Object, + **kwargs + ) + +def triprop_texture(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Object: + user_poll = kwargs.pop("poll", None) + + def poll_proc(self, value: bpy.types.Texture) -> bool: + target_material = getattr(self, material_attr) + target_object = getattr(self, object_attr) + if target_object is None: + target_object = getattr(self, "id_data", None) + + # must be a legal option... but is it a member of this material... or, if no material, + # any of the materials attached to the object? + if target_material is not None: + result = value.name in target_material.texture_slots + elif target_object is not None: + for i in (slot.material for slot in target_object.material_slots if slot and slot.material): + if value in (slot.texture for slot in i.texture_slots if slot and slot.texture): + result = True + break + else: + result = False + else: + result = False + + # Is it animated? + if result and target_material is not None: + result = ( + (target_material.animation_data is not None and target_material.animation_data.action is not None) + or (value.animation_data is not None and value.animation_data.action is not None) + ) + + # Downstream processing, if any. + if result and user_poll: + result = user_poll(self, value) + return result + + assert not "type" in kwargs + return PointerProperty( + type=bpy.types.Texture, + poll=poll_proc, + **kwargs + ) + @bpy.app.handlers.persistent def _upgrade_node_trees(dummy): """ diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index af1b5e1..ac1e582 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -21,6 +21,7 @@ from PyHSPlasma import * from typing import * +from .. import enum_props from .node_core import * from ..properties.modifiers.physics import subworld_types from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids @@ -113,39 +114,30 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, ("TEXTURE", "Texture", "Texture Action")], default="OBJECT") - def _poll_texture(self, value): - # must be a legal option... but is it a member of this material... or, if no material, - # any of the materials attached to the object? - if self.target_material is not None: - return value.name in self.target_material.texture_slots - elif self.target_object is not None: - for i in (slot.material for slot in self.target_object.material_slots if slot and slot.material): - if value in (slot.texture for slot in i.texture_slots if slot and slot.texture): - return True - return False + def _poll_target_object(self, value: bpy.types.Object) -> bool: + if self.anim_type == "TEXTURE": + return idprops.poll_drawable_objects(self, value) + elif self.anim_type == "MESH": + return idprops.poll_animated_objects(self, value) else: - return True - - def _poll_material(self, value): - # Don't filter materials by texture - this would (potentially) result in surprising UX - # in that you would have to clear the texture selection before being able to select - # certain materials. - if self.target_object is not None: - object_materials = (slot.material for slot in self.target_object.material_slots if slot and slot.material) - return value in object_materials - return True + raise RuntimeError() - target_object = PointerProperty(name="Object", - description="Target object", - type=bpy.types.Object) - target_material = PointerProperty(name="Material", - description="Target material", - type=bpy.types.Material, - poll=_poll_material) - target_texture = PointerProperty(name="Texture", - description="Target texture", - type=bpy.types.Texture, - poll=_poll_texture) + target_object = idprops.triprop_object( + "target_object", "target_material", "target_texture", + name="Object", + description="Target object", + poll=_poll_target_object + ) + target_material = idprops.triprop_material( + "target_object", "target_material", "target_texture", + name="Material", + description="Target material" + ) + target_texture = idprops.triprop_texture( + "target_object", "target_material", "target_texture", + name="Texture", + description="Target texture" + ) go_to = EnumProperty(name="Go To", description="Where should the animation start?", @@ -205,37 +197,14 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, ("kStop", "Stop", "When the action is stopped by a message")], default="kEnd") - # Blender memory workaround - _ENTIRE_ANIMATION = "(Entire Animation)" def _get_anim_names(self, context): if self.anim_type == "OBJECT": - items = [(anim.animation_name, anim.animation_name, "") - for anim in self.target_object.plasma_modifiers.animation.subanimations] + return enum_props._get_object_animation_names(self, "target_object") elif self.anim_type == "TEXTURE": - if self.target_texture is not None: - items = [(anim.animation_name, anim.animation_name, "") - for anim in self.target_texture.plasma_layer.subanimations] - elif self.target_material is not None or self.target_object is not None: - if self.target_material is None: - materials = (i.material for i in self.target_object.material_slots if i and i.material) - else: - materials = (self.target_material,) - layer_props = (i.texture.plasma_layer for mat in materials for i in mat.texture_slots if i and i.texture) - all_anims = frozenset((anim.animation_name for i in layer_props for anim in i.subanimations)) - items = [(i, i, "") for i in all_anims] - else: - items = [(PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, "")] + return enum_props._get_texture_animation_names(self, "target_object", "target_material", "target_texture") else: raise RuntimeError() - # We always want "(Entire Animation)", if it exists, to be the first item. - entire = items.index((PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, "")) - if entire not in (-1, 0): - items.pop(entire) - items.insert(0, (PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, "")) - - return items - anim_name = EnumProperty(name="Animation", description="Name of the animation to control", items=_get_anim_names, diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index a86d992..432a02a 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -21,6 +21,7 @@ from contextlib import contextmanager from pathlib import Path from PyHSPlasma import * +from .. import enum_props from .node_core import * from .node_deprecated import PlasmaDeprecatedNode, PlasmaVersionedNode from .. import idprops @@ -823,81 +824,43 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList", "ptAttribDynamicMap", "ptAttribMaterialAnimation") - def _poll_material(self, value: bpy.types.Material) -> bool: - # Don't filter materials by texture - this would (potentially) result in surprising UX - # in that you would have to clear the texture selection before being able to select - # certain materials. - if self.target_object is not None: - object_materials = (slot.material for slot in self.target_object.material_slots if slot and slot.material) - return value in object_materials - return True - def _poll_texture(self, value: bpy.types.Texture) -> bool: # is this the type of dealio that we're looking for? attrib = self.to_socket if attrib is not None: attrib = attrib.attribute_type - if attrib == "ptAttribDynamicMap": - if not self._is_dyntext(value): - return False - elif attrib == "ptAttribMaterialAnimation": - if not self._is_animated(self.material, value): - return False - - # must be a legal option... but is it a member of this material... or, if no material, - # any of the materials attached to the object? - if self.material is not None: - return value.name in self.material.texture_slots - elif self.target_object is not None: - for i in (slot.material for slot in self.target_object.material_slots if slot and slot.material): - if value in (slot.texture for slot in i.texture_slots if slot and slot.texture): - return True + if attrib == "ptAttribDynamicMap" and self._is_dyntext(value): + return True + elif attrib == "ptAttribMaterialAnimation" and not self._is_dyntext: + return True return False - else: - return True - target_object = PointerProperty(name="Object", - description="", - type=bpy.types.Object, - poll=idprops.poll_drawable_objects) - material = PointerProperty(name="Material", - description="Material the texture is attached to", - type=bpy.types.Material, - poll=_poll_material) - texture = PointerProperty(name="Texture", - description="Texture to expose to Python", - type=bpy.types.Texture, - poll=_poll_texture) - - # Blender memory workaround - _ENTIRE_ANIMATION = "(Entire Animation)" - def _get_anim_names(self, context): - if self.texture is not None: - items = [(anim.animation_name, anim.animation_name, "") - for anim in self.texture.plasma_layer.subanimations] - elif self.material is not None or self.target_object is not None: - if self.material is None: - materials = (i.material for i in self.target_object.material_slots if i and i.material) - else: - materials = (self.material,) - layer_props = (i.texture.plasma_layer for mat in materials for i in mat.texture_slots if i and i.texture) - all_anims = frozenset((anim.animation_name for i in layer_props for anim in i.subanimations)) - items = [(i, i, "") for i in all_anims] - else: - items = [(PlasmaAttribTextureNode._ENTIRE_ANIMATION, PlasmaAttribTextureNode._ENTIRE_ANIMATION, "")] - - # We always want "(Entire Animation)", if it exists, to be the first item. - entire = items.index((PlasmaAttribTextureNode._ENTIRE_ANIMATION, PlasmaAttribTextureNode._ENTIRE_ANIMATION, "")) - if entire not in (-1, 0): - items.pop(entire) - items.insert(0, (PlasmaAttribTextureNode._ENTIRE_ANIMATION, PlasmaAttribTextureNode._ENTIRE_ANIMATION, "")) - - return items + # We're not hooked up to a PFM node yet, so let anything slide. + return True - anim_name = EnumProperty(name="Animation", - description="Name of the animation to control", - items=_get_anim_names, - options=set()) + target_object = idprops.triprop_object( + "target_object", "material", "texture", + name="Object", + description="Target object" + ) + material = idprops.triprop_material( + "target_object", "material", "texture", + name="Material", + description="Material the texture is attached to" + ) + texture = idprops.triprop_texture( + "target_object", "material", "texture", + name="Texture", + description="Texture to expose to Python", + poll=_poll_texture + ) + + anim_name = enum_props.triprop_animation( + "target_object", "material", "texture", + name="Animation", + description="Name of the animation to control", + options=set() + ) def init(self, context): super().init(context) @@ -967,10 +930,6 @@ class PlasmaAttribTextureNode(idprops.IDPropMixin, PlasmaAttribNodeBase, bpy.typ return {"material_name": bpy.data.materials, "texture_name": bpy.data.textures} - def _is_animated(self, material, texture): - return ((material.animation_data is not None and material.animation_data.action is not None) - or (texture.animation_data is not None and texture.animation_data.action is not None)) - def _is_dyntext(self, texture): return texture.type == "IMAGE" and texture.image is None diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index d930264..fa3f2cf 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -203,26 +203,6 @@ class GameGuiAnimation(bpy.types.PropertyGroup): else: return idprops.poll_drawable_objects(self, value) - def _poll_texture(self, value): - # must be a legal option... but is it a member of this material... or, if no material, - # any of the materials attached to the object? - if self.target_material is not None: - return value.name in self.target_material.texture_slots - else: - target_object = self.target_object if self.target_object is not None else self.id_data - for i in (slot.material for slot in target_object.material_slots if slot and slot.material): - if value in (slot.texture for slot in i.texture_slots if slot and slot.texture): - return True - return False - - def _poll_material(self, value): - # Don't filter materials by texture - this would (potentially) result in surprising UX - # in that you would have to clear the texture selection before being able to select - # certain materials. - target_object = self.target_object if self.target_object is not None else self.id_data - object_materials = (slot.material for slot in target_object.material_slots if slot and slot.material) - return value in object_materials - anim_type: str = EnumProperty( name="Type", description="Animation type to affect", @@ -233,23 +213,21 @@ class GameGuiAnimation(bpy.types.PropertyGroup): default="OBJECT", options=set() ) - target_object: bpy.types.Object = PointerProperty( + target_object: bpy.types.Object = idprops.triprop_object( + "target_object", "target_material", "target_texure", name="Object", description="Target object", poll=_poll_target_object, - type=bpy.types.Object ) - target_material: bpy.types.Material = PointerProperty( + target_material: bpy.types.Material = idprops.triprop_material( + "target_object", "target_material", "target_texure", name="Material", description="Target material", - type=bpy.types.Material, - poll=_poll_material ) - target_texture: bpy.types.Texture = PointerProperty( + target_texture: bpy.types.Texture = idprops.triprop_texture( + "target_object", "target_material", "target_texure", name="Texture", description="Target texture", - type=bpy.types.Texture, - poll=_poll_texture )