# This file is part of Korman. # # Korman is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Korman is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Korman. If not, see . import bpy from bpy.props import * import abc from typing import Any, Dict, Generator, Optional class PlasmaModifierProperties(bpy.types.PropertyGroup): @property def copy_material(self): """Materials MUST be single-user""" return False def created(self): pass def destroyed(self): pass @property def draw_opaque(self): """Render geometry before the avatar""" return False @property def draw_framebuf(self): """Render geometry after the avatar but before other blended geometry""" return False @property def draw_no_defer(self): """Disallow geometry being sorted into a blending span""" return False @property def draw_late(self): return False @property def enabled(self): return self.display_order >= 0 def export(self, exporter, bo, so): """This is the main phase of the modifier export where most, if not all, PRP objects should be generated. No new Blender objects should be created unless their lifespan is constrained to the duration of this method. """ pass # Commented out to prevent conflicts with TranslationMixin overload. """ def export_localization(self, exporter): '''This is an auxiliary export phase that should only convert localization data. PRP objects are in an undefined state and therefore should not be used. ''' pass """ @property def face_sort(self): """Indicates that the geometry's faces should be sorted by the engine""" return False def harvest_actors(self): return () @property def key_name(self): return self.id_data.name @property def no_face_sort(self): """Indicates that the geometry's faces should never be sorted by the engine""" return False @property def no_span_sort(self): """Indicates that the geometry's Spans should never be sorted with those from other Drawables that will render in the same pass""" return False # This is temporarily commented out to prevent MRO failure. Revisit in Python 3.7 ''' def pre_export(self, exporter, bo: bpy.types.Object) -> Generator: """This is the first phase of the modifier export; allowing modifiers to create additonal objects or logic nodes to be used by the exporter. To do so, overload this method and yield any Blender ID from your method. That ID will then be exported and deleted when the export completes. PRP objects should generally not be exported in this phase. """ yield ''' @property def requires_actor(self): """Indicates if this modifier requires the object to be a movable actor""" return False # Guess what? # You can't register properties on a base class--Blender isn't smart enough to do inheritance, # you see... So, we'll store our definitions in a dict and make those properties on each subclass # at runtime. What joy. Python FTW. See register() in __init__.py _subprops = { "display_order": (IntProperty, {"name": "INTERNAL: Display Ordering", "description": "Position in the list of buttons", "default": -1, "options": {"HIDDEN"}}), "show_expanded": (BoolProperty, {"name": "INTERNAL: Actually draw the modifier", "default": True, "options": {"HIDDEN"}}), "current_version": (IntProperty, {"name": "INTERNAL: Modifier version", "default": 1, "options": {"HIDDEN"}}), } class PlasmaModifierLogicWiz: def convert_logic(self, bo, **kwargs): """Creates, converts, and returns an unmanaged NodeTree for this logic wizard. If the wizard fails during conversion, the temporary tree is deleted for you. However, on success, you are responsible for removing the tree from Blender, if applicable.""" name = kwargs.pop("name", self.key_name) assert not "tree" in kwargs tree = bpy.data.node_groups.new(name, "PlasmaNodeTree") kwargs["tree"] = tree try: self.logicwiz(bo, **kwargs) except: bpy.data.node_groups.remove(tree) raise else: return tree def _create_python_file_node(self, tree, filename: str, attributes: Dict[str, Any]) -> bpy.types.Node: pfm_node = tree.nodes.new("PlasmaPythonFileNode") with pfm_node.NoUpdate(): pfm_node.filename = filename for attr in attributes: new_attr = pfm_node.attributes.add() new_attr.attribute_id = attr["id"] new_attr.attribute_type = attr["type"] new_attr.attribute_name = attr["name"] pfm_node.update() return pfm_node def _create_python_attribute(self, pfm_node, attribute_name: str, attribute_type: Optional[str] = None, **kwargs): """Creates and links a Python Attribute Node to the Python File Node given by `pfm_node`. This will automatically handle simple attribute types such as numbers and strings, however, for object linkage, you should specify the optional `attribute_type` to ensure the proper attribute type is found. For attribute nodes that require multiple values, the `value` may be set to None and handled in your code.""" from ...nodes.node_python import PlasmaAttribute, PlasmaAttribNodeBase if attribute_type is None: assert len(kwargs) == 1 and "value" in kwargs, \ "In order to deduce the attribute_type, exactly one attribute value must be passed as a kw named `value`" attribute_type = PlasmaAttribute.type_LUT.get(kwargs["value"].__class__) node_cls = next((i for i in PlasmaAttribNodeBase.__subclasses__() if attribute_type in i.pl_attrib), None) assert node_cls is not None, "'{}': Unable to find attribute node type for '{}' ('{}')".format( self.id_data.name, attribute_name, attribute_type ) node = pfm_node.id_data.nodes.new(node_cls.bl_idname) node.link_output(pfm_node, "pfm", attribute_name) for i, j in kwargs.items(): setattr(node, i, j) return node @abc.abstractmethod def logicwiz(self, bo, tree): pass def pre_export(self, exporter, bo): """Default implementation of the pre_export phase for logic wizards that simply triggers the logic nodes to be created and for their export to be scheduled.""" yield self.convert_logic(bo) class PlasmaModifierUpgradable: @property @abc.abstractmethod def latest_version(self): raise NotImplementedError() @property def requires_upgrade(self): current_version, latest_version = self.current_version, self.latest_version assert current_version <= latest_version return current_version < latest_version @abc.abstractmethod def upgrade(self): raise NotImplementedError() @bpy.app.handlers.persistent def _restore_properties(dummy): # When Blender opens, it loads the default blend. The post load handler # below is executed and deprecated properties are unregistered. When the # user goes to load a new blend file, the handler below tries to execute # again and BOOM--there are no deprecated properties available. Therefore, # we reregister them here. for mod_cls in PlasmaModifierUpgradable.__subclasses__(): for prop_name in mod_cls.deprecated_properties: # Unregistered propertes are a sequence of (property function, # property keyword arguments). Interesting design decision :) prop_cb, prop_kwargs = getattr(mod_cls, prop_name) del prop_kwargs["attr"] # Prevents proper registration setattr(mod_cls, prop_name, prop_cb(**prop_kwargs)) bpy.app.handlers.load_pre.append(_restore_properties) @bpy.app.handlers.persistent def _upgrade_modifiers(dummy): # First, run all the upgrades for i in bpy.data.objects: for mod_cls in PlasmaModifierUpgradable.__subclasses__(): mod = getattr(i.plasma_modifiers, mod_cls.pl_id) if mod.requires_upgrade: mod.upgrade() # Now that everything is upgraded, forcibly remove all properties # from the modifiers to prevent sneaky zombie-data type export bugs for mod_cls in PlasmaModifierUpgradable.__subclasses__(): for prop in mod_cls.deprecated_properties: RemoveProperty(mod_cls, attr=prop) bpy.app.handlers.load_post.append(_upgrade_modifiers)