Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bounds and Animation Property Deduplication #415

Merged
merged 3 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions korman/enum_props.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# 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/>.

from __future__ import annotations

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 = (
("box", "Bounding Box", "Use a perfect bounding box"),
("sphere", "Bounding Sphere", "Use a perfect bounding sphere"),
("hull", "Convex Hull", "Use a convex set encompasing all vertices"),
("trimesh", "Triangle Mesh", "Use the exact triangle mesh (SLOW!)")
)

def _bounds_type_index(key: str) -> int:
return list(zip(*_bounds_types))[0].index(key)

def _bounds_type_str(idx: int) -> str:
return _bounds_types[idx][0]

def _get_bounds(physics_attr: Optional[str]) -> Callable[[Any], int]:
def getter(self) -> int:
physics_object = getattr(self, physics_attr) if physics_attr is not None else self.id_data
if physics_object is not None:
return _bounds_type_index(physics_object.plasma_modifiers.collision.bounds)
return _bounds_type_index("hull")
return getter

def _set_bounds(physics_attr: Optional[str]) -> Callable[[Any, int], None]:
def setter(self, value: int):
physics_object = getattr(self, physics_attr) if physics_attr is not None else self.id_data
if physics_object is not None:
physics_object.plasma_modifiers.collision.bounds = _bounds_type_str(value)
return setter

def bounds(physics_attr: Optional[str] = None, store_on_collider: bool = True, **kwargs) -> str:
assert not {"items", "get", "set"} & kwargs.keys(), "You cannot use the `items`, `get`, or `set` keyword arguments"
if store_on_collider:
kwargs["get"] = _get_bounds(physics_attr)
kwargs["set"] = _set_bounds(physics_attr)
else:
warnings.warn("Storing bounds properties outside of the collision modifier is deprecated.", category=DeprecationWarning)
if "default" not in kwargs:
kwargs["default"] = "hull"
return EnumProperty(
items=_bounds_types,
**kwargs
)

def upgrade_bounds(bl, bounds_attr: str) -> None:
# Only perform this process if the property has a value. Otherwise, we'll
# wind up blowing away the collision modifier's settings with nonsense.
if not bl.is_property_set(bounds_attr):
return

# Before we unregister anything, grab a copy of what the collision modifier currently thinks.
bounds_value_curr = getattr(bl, bounds_attr)

# So, here's the deal. If someone has been playing with nodes and changed the bounds type,
# Blender will think the property has been set, even if they wound up with the property
# at the default value. I don't know that we can really trust the default in the property
# definition to be the same as the old default (they shouldn't be different, but let's be safe).
# So, let's apply rough justice. If the destination property thinks it's a triangle mesh, we
# don't need to blow that away - it's a very specific non default setting.
if bounds_value_curr == "trimesh":
return

# Unregister the new/correct proxy bounds property (with getter/setter) and re-register
# the property without the proxy functions to get the old value. Reregister the new property
# again and set it.
cls = bl.__class__
prop_func, prop_def = getattr(cls, bounds_attr)
RemoveProperty(cls, attr=bounds_attr)
del prop_def["attr"]

# Remove the things we don't want in a copy to prevent hosing the new property.
old_prop_def = dict(prop_def)
del old_prop_def["get"]
del old_prop_def["set"]
setattr(cls, bounds_attr, prop_func(**old_prop_def))
bounds_value_new = getattr(bl, bounds_attr)

# Re-register new property.
RemoveProperty(cls, attr=bounds_attr)
setattr(cls, bounds_attr, prop_func(**prop_def))

# Only set the property if the value different to avoid thrashing and log spam.
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)
80 changes: 80 additions & 0 deletions korman/idprops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
74 changes: 59 additions & 15 deletions korman/nodes/node_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
from PyHSPlasma import *
from typing import *

from .. import enum_props
from .node_core import *
from ..properties.modifiers.physics import bounds_types
from .node_deprecated import PlasmaVersionedNode
from .. import idprops

class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableNode"
bl_label = "Clickable"
Expand All @@ -38,10 +39,13 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
description="Mesh object that is clickable",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds",
description="Clickable's bounds (NOTE: only used if your clickable is not a collider)",
items=bounds_types,
default="hull")

bounds = enum_props.bounds(
"clickable_object",
name="Bounds",
description="Clickable's bounds",
default="hull"
)

input_sockets: dict[str, Any] = {
"region": {
Expand Down Expand Up @@ -151,8 +155,20 @@ def requires_actor(self):
def _idprop_mapping(cls):
return {"clickable_object": "clickable"}

@property
def latest_version(self):
return 2

def upgrade(self):
# In version 1 of this node, the bounds type was stored on this node. This could
# be overridden by whatever was in the collision modifier. Version 2 changes the
# bounds property to proxy to the collision modifier's bounds settings.
if self.version == 2:
enum_props.upgrade_bounds(self, "bounds")
self.version = 2

class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):

class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableRegionNode"
bl_label = "Clickable Region Settings"
Expand All @@ -162,10 +178,12 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds",
description="Physical object's bounds (NOTE: only used if your clickable is not a collider)",
items=bounds_types,
default="hull")
bounds = enum_props.bounds(
"region_object",
name="Bounds",
description="Physical object's bounds",
default="hull"
)

output_sockets = {
"satisfies": {
Expand Down Expand Up @@ -215,6 +233,18 @@ def convert_subcondition(self, exporter, parent_bo, parent_so, logicmod):
def _idprop_mapping(cls):
return {"region_object": "region"}

@property
def latest_version(self):
return 2

def upgrade(self):
# In version 1 of this node, the bounds type was stored on this node. This could
# be overridden by whatever was in the collision modifier. Version 2 changes the
# bounds property to proxy to the collision modifier's bounds settings.
if self.version == 1:
enum_props.upgrade_bounds(self, "bounds")
self.version = 2


class PlasmaClickableRegionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.412, 0.0, 0.055, 1.0)
Expand Down Expand Up @@ -395,7 +425,7 @@ def draw_buttons(self, context, layout):
row.prop(self, "threshold", text="")


class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaVolumeSensorNode"
bl_label = "Region Sensor"
Expand All @@ -419,9 +449,11 @@ def _update_report_on(self, context):
description="Object that defines the region mesh",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects)
bounds = EnumProperty(name="Bounds",
description="Physical object's bounds",
items=bounds_types)
bounds = enum_props.bounds(
"region_object",
name="Bounds",
description="Physical object's bounds"
)

# Detector Properties
report_on = EnumProperty(name="Triggerers",
Expand Down Expand Up @@ -586,6 +618,18 @@ def report_exits(self):
return (self.find_input_socket("exit").allow or
self.find_input("exit", "PlasmaVolumeReportNode") is not None)

@property
def latest_version(self):
return 2

def upgrade(self):
# In version 1 of this node, the bounds type was stored on this node. This could
# be overridden by whatever was in the collision modifier. Version 2 changes the
# bounds property to proxy to the collision modifier's bounds settings.
if self.version == 1:
enum_props.upgrade_bounds(self, "bounds")
self.version = 2


class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase):
bl_color = (43.1, 24.7, 0.0, 1.0)
Expand Down
Loading