|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
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)
|