From ef9e17af618ea58298bf987f220fc8e18bfbbd9b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 4 Jul 2014 23:51:02 -0400 Subject: [PATCH] Basic Modifiers So this is a fairly massive chunk. I tried to get our Plasma Modifiers to match the Blender Modifier UI fairly well. In the process, I discovered that Blender "helpfully" hides the modifier button on Empty objects. Bah. Significant Changes: - Hid all of the Blender Physics mess - The Physics context is now the Plasma Object context - Moved Plasma Object and Plasma Synchronization to the Context Formerly Known as Physics - Added a Plasma Modifier Panel Here's how you create Plasma Modifiers: - Add your PropertyGroup to properties.modifiers - Make sure you have a pl_id naming your modifier, a category, and a label - Implement the export() method to actually export something useful (not goat porn, please) - Implement your UI draw function in ui.modifiers. - Wasn't that easy? --- korman/__init__.py | 3 +- korman/exporter/convert.py | 5 + korman/operators/__init__.py | 1 + korman/operators/op_modifier.py | 125 ++++++++++++++++++++++++ korman/properties/__init__.py | 12 ++- korman/properties/modifiers/__init__.py | 90 +++++++++++++++++ korman/properties/modifiers/base.py | 45 +++++++++ korman/properties/modifiers/logic.py | 35 +++++++ korman/render.py | 7 ++ korman/ui/__init__.py | 1 + korman/ui/modifiers/__init__.py | 16 +++ korman/ui/modifiers/logic.py | 18 ++++ korman/ui/ui_modifiers.py | 89 +++++++++++++++++ korman/ui/ui_object.py | 20 +++- 14 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 korman/operators/op_modifier.py create mode 100644 korman/properties/modifiers/__init__.py create mode 100644 korman/properties/modifiers/base.py create mode 100644 korman/properties/modifiers/logic.py create mode 100644 korman/ui/modifiers/__init__.py create mode 100644 korman/ui/modifiers/logic.py create mode 100644 korman/ui/ui_modifiers.py diff --git a/korman/__init__.py b/korman/__init__.py index 01a81a1..940b014 100644 --- a/korman/__init__.py +++ b/korman/__init__.py @@ -15,7 +15,8 @@ import bpy from . import exporter, render -from . import operators, properties, ui +from . import properties, ui +from . import operators bl_info = { "name": "Korman", diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 95ecba6..2b4c714 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -148,6 +148,11 @@ class Exporter: self._export_actor(sceneobject, bl_obj) export_fn(sceneobject, bl_obj) + # And now we puke out the modifiers... + for mod in bl_obj.plasma_modifiers.modifiers: + print(" Exporting '{}' modifier as '{}'".format(mod.bl_label, mod.display_name)) + mod.export(self, bl_obj, sceneobject) + def _export_empty_blobj(self, so, bo): # We don't need to do anything here. This function just makes sure we don't error out # or add a silly special case :( diff --git a/korman/operators/__init__.py b/korman/operators/__init__.py index fc9150b..12cf63f 100644 --- a/korman/operators/__init__.py +++ b/korman/operators/__init__.py @@ -14,6 +14,7 @@ # along with Korman. If not, see . from . import op_export as exporter +from . import op_modifier as modifier from . import op_world as world def register(): diff --git a/korman/operators/op_modifier.py b/korman/operators/op_modifier.py new file mode 100644 index 0000000..0a9526c --- /dev/null +++ b/korman/operators/op_modifier.py @@ -0,0 +1,125 @@ +# 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 * + +from ..properties import modifiers + +def _fetch_modifiers(): + items = [] + + mapping = modifiers.modifier_mapping() + for i in mapping.keys(): + items.append(("", i, "")) + items.extend(mapping[i]) + #yield ("", i, "") + #yield mapping[i] + return items + +class ModifierOperator: + @classmethod + def poll(cls, context): + return context.scene.render.engine == "PLASMA_GAME" + + +class ModifierAddOperator(ModifierOperator, bpy.types.Operator): + bl_idname = "object.plasma_modifier_add" + bl_label = "Add Modifier" + bl_description = "Adds a Plasma Modifier" + + types = EnumProperty(name="Modifier Type", + description="The type of modifier we add to the list", + items=_fetch_modifiers()) + + def execute(self, context): + plmods = context.object.plasma_modifiers + myType = self.types + theMod = getattr(plmods, myType) + + theMod.display_order = plmods.determine_next_id() + theMod.created(context.object) + return {"FINISHED"} + + +class ModifierRemoveOperator(ModifierOperator, bpy.types.Operator): + bl_idname = "object.plasma_modifier_remove" + bl_label = "Remove Modifier" + bl_description = "Removes this Plasma Modifier" + + active_modifier = IntProperty(name="Modifier Display Order", + default=-1, + options={"HIDDEN"}) + + def execute(self, context): + assert self.active_modifier >= 0 + + for mod in context.object.plasma_modifiers.modifiers: + if mod.display_order == self.active_modifier: + mod.display_order = -1 + mod.destroyed() + elif mod.display_order > self.active_modifier: + mod.display_order -= 1 + return {"FINISHED"} + + +class ModifierMoveOperator(ModifierOperator): + def swap_modifier_ids(self, mods, s1, s2): + done = 0 + for mod in mods.modifiers: + if mod.display_order == s1: + mod.display_order = s2 + done += 1 + elif mod.display_order == s2: + mod.display_order = s1 + done += 1 + if done == 2: + break + + +class ModifierMoveUpOperator(ModifierMoveOperator, bpy.types.Operator): + bl_idname = "object.plasma_modifier_move_up" + bl_label = "Move Up" + bl_description = "Move the modifier up in the stack" + + active_modifier = IntProperty(name="Modifier Display Order", + default=-1, + options={"HIDDEN"}) + + def execute(self, context): + assert self.active_modifier >= 0 + if self.active_modifier > 0: + plmods = context.object.plasma_modifiers + self.swap_modifier_ids(plmods, self.active_modifier, self.active_modifier-1) + return {"FINISHED"} + + +class ModifierMoveDownOperator(ModifierMoveOperator, bpy.types.Operator): + bl_idname = "object.plasma_modifier_move_down" + bl_label = "Move Down" + bl_description = "Move the modifier down in the stack" + + active_modifier = IntProperty(name="Modifier Display Order", + default=-1, + options={"HIDDEN"}) + + def execute(self, context): + assert self.active_modifier >= 0 + + plmods = context.object.plasma_modifiers + last = max([mod.display_order for mod in plmods.modifiers]) + if self.active_modifier < last: + self.swap_modifier_ids(plmods, self.active_modifier, self.active_modifier+1) + return {"FINISHED"} diff --git a/korman/properties/__init__.py b/korman/properties/__init__.py index 7d67d15..ec3a52d 100644 --- a/korman/properties/__init__.py +++ b/korman/properties/__init__.py @@ -15,12 +15,16 @@ import bpy +from . import modifiers from .prop_object import * from .prop_world import * def register(): - bpy.types.Object.plasma_net = PointerProperty(type=PlasmaNet) - bpy.types.Object.plasma_object = PointerProperty(type=PlasmaObject) - bpy.types.World.plasma_age = PointerProperty(type=PlasmaAge) - bpy.types.World.plasma_fni = PointerProperty(type=PlasmaFni) + bpy.types.Object.plasma_net = bpy.props.PointerProperty(type=PlasmaNet) + bpy.types.Object.plasma_object = bpy.props.PointerProperty(type=PlasmaObject) + bpy.types.World.plasma_age = bpy.props.PointerProperty(type=PlasmaAge) + bpy.types.World.plasma_fni = bpy.props.PointerProperty(type=PlasmaFni) + + # We have our own brand of special insanity in the modifier code, so let's handle that in there + modifiers.register() diff --git a/korman/properties/modifiers/__init__.py b/korman/properties/modifiers/__init__.py new file mode 100644 index 0000000..2179d9f --- /dev/null +++ b/korman/properties/modifiers/__init__.py @@ -0,0 +1,90 @@ +# 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 +import inspect + +from .base import PlasmaModifierProperties +from .logic import * + +class PlasmaModifiers(bpy.types.PropertyGroup): + def determine_next_id(self): + """Gets the ID for the next modifier in the UI""" + # This is NOT a property, otherwise the modifiers property would access this... + # Which acesses the modifiers property... INFINITE RECURSION! :D + ids = [mod.display_order for mod in self.modifiers] + if ids: + return max(ids) + 1 + else: + return 0 + + @property + def modifiers(self): + """Generates all of the enabled modifiers. + NOTE: We do not promise to return modifiers in their display_order! + """ + for i in dir(self): + attr = getattr(self, i) + # Assumes each modifier is a single pointer to PlasmaModifierProperties + if isinstance(attr, PlasmaModifierProperties): + if attr.enabled: + yield attr + + +def _is_plasma_modifier(hClass): + if inspect.isclass(hClass): + if issubclass(hClass, PlasmaModifierProperties) and hasattr(hClass, "pl_id"): + return True + return False + +def modifier_definitions(): + """This returns a sequence of all modifiers""" + for i in globals().values(): + if _is_plasma_modifier(i): + yield i + +def modifier_mapping(): + """This returns a dict mapping Plasma Modifier categories to names""" + + # FIXME: a more pythonic way to do this??? + d = {} + for i, mod in enumerate(modifier_definitions()): + if hasattr(mod, "bl_icon"): + icon = mod.bl_icon + else: + icon = "" + + tup = (mod.pl_id, mod.bl_label, mod.bl_description, icon, i) + if mod.bl_category not in d: + d[mod.bl_category] = [tup] + else: + d[mod.bl_category].append(tup) + return d + +def register(): + # Okay, so we have N plasma modifer property groups... + # Rather than have (dis)organized chaos on the Blender Object, we will collect all of the + # property groups of type PlasmaModifierProperties and generate on-the-fly a PlasmaModifier + # property group to rule them all. The class attribute 'pl_id' will determine the name of + # the property group in PlasmaModifiers. + # Also, just to spite us, Blender doesn't seem to handle PropertyGroup inheritance... at all. + # So, we're going to have to create our base properties on all of the PropertyGroups. + # It's times like these that make me wonder about life... + # Enjoy! + for i in modifier_definitions(): + for name, (prop, kwargs) in PlasmaModifierProperties._subprops.items(): + setattr(i, name, prop(**kwargs)) + setattr(PlasmaModifiers, i.pl_id, bpy.props.PointerProperty(type=i)) + bpy.types.Object.plasma_modifiers = bpy.props.PointerProperty(type=PlasmaModifiers) diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py new file mode 100644 index 0000000..53a1992 --- /dev/null +++ b/korman/properties/modifiers/base.py @@ -0,0 +1,45 @@ +# 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 * + +class PlasmaModifierProperties(bpy.types.PropertyGroup): + def created(self, obj): + # This is here just to prevent us from having unnamed modifiers + self.display_name = "{}Modifier{}".format(obj.name, self.display_order) + + def destroyed(self): + pass + + @property + def enabled(self): + return self.display_order >= 0 + + # 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_name": (StringProperty, {"name": "Name", + "description": "Modifier name"}), + "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"}}) + } diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py new file mode 100644 index 0000000..2facf55 --- /dev/null +++ b/korman/properties/modifiers/logic.py @@ -0,0 +1,35 @@ +# 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 PyHSPlasma import * + +from .base import PlasmaModifierProperties + +class PlasmaSpawnPoint(PlasmaModifierProperties): + pl_id = "spawnpoint" + + bl_category = "Logic" + bl_label = "Spawn Point" + bl_description = "Point at which avatars link into the Age" + + def created(self, obj): + self.display_name = obj.name + + def export(self, exporter, bo, so): + # Not much to this modifier... It's basically a flag that tells the engine, "hey, this is a + # place the avatar can show up." Nice to have a simple one to get started with. + spawn = exporter.mgr.add_object(pl=plSpawnModifier, bl=bo, name=self.display_name) + so.addModifier(spawn.key) diff --git a/korman/render.py b/korman/render.py index ac16ca0..207e0be 100644 --- a/korman/render.py +++ b/korman/render.py @@ -51,3 +51,10 @@ def _new_poll(cls, context): def _swap_poll(cls): cls._old_poll = cls.poll cls.poll = _new_poll + +# This is where we claim the physics context for our own nefarious purposes... +# Hmm... Physics panels don't respect the supported engine thing. +# Metaprogramming to the rescue! +from bl_ui import properties_physics_common +_swap_poll(properties_physics_common.PhysicButtonsPanel) +del properties_physics_common diff --git a/korman/ui/__init__.py b/korman/ui/__init__.py index 00688be..1416d1e 100644 --- a/korman/ui/__init__.py +++ b/korman/ui/__init__.py @@ -14,4 +14,5 @@ # along with Korman. If not, see . from .ui_object import * +from .ui_modifiers import * from .ui_world import * diff --git a/korman/ui/modifiers/__init__.py b/korman/ui/modifiers/__init__.py new file mode 100644 index 0000000..a8b2222 --- /dev/null +++ b/korman/ui/modifiers/__init__.py @@ -0,0 +1,16 @@ +# 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 . + +from .logic import * diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py new file mode 100644 index 0000000..08cc998 --- /dev/null +++ b/korman/ui/modifiers/logic.py @@ -0,0 +1,18 @@ +# 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 . + +def spawnpoint(modifier, layout, context): + layout.label(text="The Y axis indicates the direction the avatar is facing.") + diff --git a/korman/ui/ui_modifiers.py b/korman/ui/ui_modifiers.py new file mode 100644 index 0000000..ffb7257 --- /dev/null +++ b/korman/ui/ui_modifiers.py @@ -0,0 +1,89 @@ +# 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 . import modifiers as modifier_draw + +class ModifierButtonsPanel: + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + + # Let me take this opportunity to rant. + # For some STUPID REASON, Blender decides which buttons to show in the C code. This is all well + # and good, EXCEPT THEY DO NOT SHOW THE MODIFIERS BUTTON FOR EMPTIES. This totally breaks the + # Plasma Modifier workflow. As a shim workaround, we're overtaking the physics panel as our + # Plasma Modifier Panel. The Physics, Object, and Constraint Panels' visibility determined by + # the same block of a switch statement in Blender 2.71 (buttons_context_path in buttons_context.c) + bl_context = "physics" + + @classmethod + def poll(cls, context): + return context.object and context.scene.render.engine == "PLASMA_GAME" + + +class PlasmaModifiersPanel(ModifierButtonsPanel, bpy.types.Panel): + bl_label = "Plasma Modifiers" + + def draw(self, context): + layout = self.layout + obj = context.object + + # So, I had to read the doggone Blender source code to figure out how to use this because the + # "documentation" only gives this helpful information about this interesting feature: "operator_menu_enum" + # Bah. For the record: first param is the operator, second is an EnumProperty on that operator. + # You define categories by inserting an enum item with an empty key, empty description, and just a name. + # Any items following that are members of that category, of course... + # ... I hope that my rambling has helped somebody understand more about the undocumented mess + # that is Blender Python. + layout.operator_menu_enum("object.plasma_modifier_add", "types") + + # First, let's sort the list of modifiers based on their display order + # We don't do this sort in the property itself because this is really just a UI hint. + modifiers = sorted(obj.plasma_modifiers.modifiers, key=lambda x: x.display_order) + + # Inside the modifier_draw module, we have draw callbables for each modifier + # We'll loop through the list of active modifiers and call the drawprocs for the enabled mods + for i in modifiers: + modLayout = self._draw_modifier_template(i) + if i.show_expanded: + getattr(modifier_draw, i.pl_id)(i, modLayout, context) + + def _draw_modifier_template(self, modifier): + """This draws our lookalike modifier template and returns a UILayout object for each modifier + to consume in order to draw its specific properties""" + layout = self.layout.box() + + # This is the main title row. It mimics the Blender template_modifier, which (unfortunately) + # requires valid Blender Modifier data. It would be nice if the Blender UI code were consistently + # C or Python and not a frankenstein mix. I would probably prefer working in C, just because + # the compiler saves my neck 99.9% of the time... + row = layout.row(align=True) + exicon = "TRIA_DOWN" if modifier.show_expanded else "TRIA_RIGHT" + row.prop(modifier, "show_expanded", text="", icon=exicon, emboss=False) + if hasattr(modifier, "bl_icon"): + row.label(icon=modifier.bl_icon) + else: + row.label(text=modifier.bl_label) + row.prop(modifier, "display_name", text="") + + row.operator("object.plasma_modifier_move_up", text="", icon="TRIA_UP").active_modifier = modifier.display_order + row.operator("object.plasma_modifier_move_down", text="", icon="TRIA_DOWN").active_modifier = modifier.display_order + row.operator("object.plasma_modifier_remove", text="", icon="X").active_modifier = modifier.display_order + + # Now we return the modifier box, which is populated with the modifier specific properties + # by whatever insanity is in the modifier module. modifier modifier modifier... + # MODDDDDDDDIFIIIIEEEERRRRRRRR!!! + return layout diff --git a/korman/ui/ui_object.py b/korman/ui/ui_object.py index 489b112..b0f869b 100644 --- a/korman/ui/ui_object.py +++ b/korman/ui/ui_object.py @@ -15,17 +15,32 @@ import bpy - class ObjectButtonsPanel: bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "object" + bl_context = "physics" @classmethod def poll(cls, context): return context.object and context.scene.render.engine == "PLASMA_GAME" +class BlenderObjectSearchPanel(ObjectButtonsPanel, bpy.types.Panel): + bl_label = "" + bl_options = {"HIDE_HEADER"} + + def draw(self, context): + # Yes, this is stolen shamelessly from bl_ui + layout = self.layout + space = context.space_data + + if space.use_pin_id: + layout.template_ID(space, "pin_id") + else: + row = layout.row() + row.template_ID(context.scene.objects, "active") + + class PlasmaObjectPanel(ObjectButtonsPanel, bpy.types.Panel): bl_label = "Plasma Object" @@ -45,6 +60,7 @@ class PlasmaObjectPanel(ObjectButtonsPanel, bpy.types.Panel): class PlasmaNetPanel(ObjectButtonsPanel, bpy.types.Panel): bl_label = "Plasma Synchronization" + bl_options = {"DEFAULT_CLOSED"} def draw_header(self, context): self.layout.prop(context.object.plasma_net, "manual_sdl", text="")