Skip to content

Commit 9dc7f68

Browse files
authored
Merge pull request #415 from Hoikas/bounds_storage_overhaul
Bounds and Animation Property Deduplication
2 parents b6e8c5b + adb8b23 commit 9dc7f68

File tree

11 files changed

+408
-229
lines changed

11 files changed

+408
-229
lines changed

korman/enum_props.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# This file is part of Korman.
2+
#
3+
# Korman is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# Korman is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
15+
16+
from __future__ import annotations
17+
18+
from bpy.props import *
19+
20+
from typing import *
21+
import warnings
22+
23+
# Workaround for Blender memory management limitation,
24+
# don't change this to a literal in the code!
25+
_ENTIRE_ANIMATION = "(Entire Animation)"
26+
27+
def _get_object_animation_names(self, object_attr: str) -> Sequence[Tuple[str, str, str]]:
28+
target_object = getattr(self, object_attr)
29+
if target_object is not None:
30+
items = [(anim.animation_name, anim.animation_name, "")
31+
for anim in target_object.plasma_modifiers.animation.subanimations]
32+
else:
33+
items = [(_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")]
34+
35+
# We always want "(Entire Animation)", if it exists, to be the first item.
36+
entire = items.index((_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, ""))
37+
if entire not in (-1, 0):
38+
items.pop(entire)
39+
items.insert(0, (_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, ""))
40+
41+
return items
42+
43+
def animation(object_attr: str, **kwargs) -> str:
44+
def get_items(self, context):
45+
return _get_object_animation_names(self, object_attr)
46+
47+
return EnumProperty(items=get_items, **kwargs)
48+
49+
# These are the kinds of physical bounds Plasma can work with.
50+
# This sequence is acceptable in any EnumProperty
51+
_bounds_types = (
52+
("box", "Bounding Box", "Use a perfect bounding box"),
53+
("sphere", "Bounding Sphere", "Use a perfect bounding sphere"),
54+
("hull", "Convex Hull", "Use a convex set encompasing all vertices"),
55+
("trimesh", "Triangle Mesh", "Use the exact triangle mesh (SLOW!)")
56+
)
57+
58+
def _bounds_type_index(key: str) -> int:
59+
return list(zip(*_bounds_types))[0].index(key)
60+
61+
def _bounds_type_str(idx: int) -> str:
62+
return _bounds_types[idx][0]
63+
64+
def _get_bounds(physics_attr: Optional[str]) -> Callable[[Any], int]:
65+
def getter(self) -> int:
66+
physics_object = getattr(self, physics_attr) if physics_attr is not None else self.id_data
67+
if physics_object is not None:
68+
return _bounds_type_index(physics_object.plasma_modifiers.collision.bounds)
69+
return _bounds_type_index("hull")
70+
return getter
71+
72+
def _set_bounds(physics_attr: Optional[str]) -> Callable[[Any, int], None]:
73+
def setter(self, value: int):
74+
physics_object = getattr(self, physics_attr) if physics_attr is not None else self.id_data
75+
if physics_object is not None:
76+
physics_object.plasma_modifiers.collision.bounds = _bounds_type_str(value)
77+
return setter
78+
79+
def bounds(physics_attr: Optional[str] = None, store_on_collider: bool = True, **kwargs) -> str:
80+
assert not {"items", "get", "set"} & kwargs.keys(), "You cannot use the `items`, `get`, or `set` keyword arguments"
81+
if store_on_collider:
82+
kwargs["get"] = _get_bounds(physics_attr)
83+
kwargs["set"] = _set_bounds(physics_attr)
84+
else:
85+
warnings.warn("Storing bounds properties outside of the collision modifier is deprecated.", category=DeprecationWarning)
86+
if "default" not in kwargs:
87+
kwargs["default"] = "hull"
88+
return EnumProperty(
89+
items=_bounds_types,
90+
**kwargs
91+
)
92+
93+
def upgrade_bounds(bl, bounds_attr: str) -> None:
94+
# Only perform this process if the property has a value. Otherwise, we'll
95+
# wind up blowing away the collision modifier's settings with nonsense.
96+
if not bl.is_property_set(bounds_attr):
97+
return
98+
99+
# Before we unregister anything, grab a copy of what the collision modifier currently thinks.
100+
bounds_value_curr = getattr(bl, bounds_attr)
101+
102+
# So, here's the deal. If someone has been playing with nodes and changed the bounds type,
103+
# Blender will think the property has been set, even if they wound up with the property
104+
# at the default value. I don't know that we can really trust the default in the property
105+
# definition to be the same as the old default (they shouldn't be different, but let's be safe).
106+
# So, let's apply rough justice. If the destination property thinks it's a triangle mesh, we
107+
# don't need to blow that away - it's a very specific non default setting.
108+
if bounds_value_curr == "trimesh":
109+
return
110+
111+
# Unregister the new/correct proxy bounds property (with getter/setter) and re-register
112+
# the property without the proxy functions to get the old value. Reregister the new property
113+
# again and set it.
114+
cls = bl.__class__
115+
prop_func, prop_def = getattr(cls, bounds_attr)
116+
RemoveProperty(cls, attr=bounds_attr)
117+
del prop_def["attr"]
118+
119+
# Remove the things we don't want in a copy to prevent hosing the new property.
120+
old_prop_def = dict(prop_def)
121+
del old_prop_def["get"]
122+
del old_prop_def["set"]
123+
setattr(cls, bounds_attr, prop_func(**old_prop_def))
124+
bounds_value_new = getattr(bl, bounds_attr)
125+
126+
# Re-register new property.
127+
RemoveProperty(cls, attr=bounds_attr)
128+
setattr(cls, bounds_attr, prop_func(**prop_def))
129+
130+
# Only set the property if the value different to avoid thrashing and log spam.
131+
if bounds_value_curr != bounds_value_new:
132+
print(f"Stashing bounds property: [{bl.name}] ({cls.__name__}) {bounds_value_curr} -> {bounds_value_new}") # TEMP
133+
setattr(bl, bounds_attr, bounds_value_new)
134+
135+
def _get_texture_animation_names(self, object_attr: str, material_attr: str, texture_attr: str) -> Sequence[Tuple[str, str, str]]:
136+
target_object = getattr(self, object_attr)
137+
material = getattr(self, material_attr)
138+
texture = getattr(self, texture_attr)
139+
140+
if texture is not None:
141+
items = [(anim.animation_name, anim.animation_name, "")
142+
for anim in texture.plasma_layer.subanimations]
143+
elif material is not None or target_object is not None:
144+
if material is None:
145+
materials = (i.material for i in target_object.material_slots if i and i.material)
146+
else:
147+
materials = (material,)
148+
layer_props = (i.texture.plasma_layer for mat in materials for i in mat.texture_slots if i and i.texture)
149+
all_anims = frozenset((anim.animation_name for i in layer_props for anim in i.subanimations))
150+
items = [(i, i, "") for i in all_anims]
151+
else:
152+
items = [(_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, "")]
153+
154+
# We always want "(Entire Animation)", if it exists, to be the first item.
155+
entire = items.index((_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, ""))
156+
if entire not in (-1, 0):
157+
items.pop(entire)
158+
items.insert(0, (_ENTIRE_ANIMATION, _ENTIRE_ANIMATION, ""))
159+
160+
return items
161+
162+
def triprop_animation(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> str:
163+
def get_items(self, context):
164+
return _get_texture_animation_names(self, object_attr, material_attr, texture_attr)
165+
166+
assert not {"items", "get", "set"} & kwargs.keys(), "You cannot use the `items`, `get`, or `set` keyword arguments"
167+
return EnumProperty(items=get_items, **kwargs)

korman/idprops.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,86 @@ def poll_visregion_objects(self, value):
145145
def poll_envmap_textures(self, value):
146146
return isinstance(value, bpy.types.EnvironmentMapTexture)
147147

148+
def triprop_material(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Material:
149+
user_poll = kwargs.pop("poll", None)
150+
151+
def poll_proc(self, value: bpy.types.Material) -> bool:
152+
target_object = getattr(self, object_attr)
153+
if target_object is None:
154+
target_object = getattr(self, "id_data", None)
155+
156+
# Don't filter materials by texture - this would (potentially) result in surprising UX
157+
# in that you would have to clear the texture selection before being able to select
158+
# certain materials.
159+
if target_object is not None:
160+
object_materials = (slot.material for slot in target_object.material_slots if slot and slot.material)
161+
result = value in object_materials
162+
else:
163+
result = True
164+
165+
# Downstream processing, if any.
166+
if result and user_poll is not None:
167+
result = user_poll(self, value)
168+
return result
169+
170+
assert not "type" in kwargs
171+
return PointerProperty(
172+
type=bpy.types.Material,
173+
poll=poll_proc,
174+
**kwargs
175+
)
176+
177+
def triprop_object(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Texture:
178+
assert not "type" in kwargs
179+
if not "poll" in kwargs:
180+
kwargs["poll"] = poll_drawable_objects
181+
return PointerProperty(
182+
type=bpy.types.Object,
183+
**kwargs
184+
)
185+
186+
def triprop_texture(object_attr: str, material_attr: str, texture_attr: str, **kwargs) -> bpy.types.Object:
187+
user_poll = kwargs.pop("poll", None)
188+
189+
def poll_proc(self, value: bpy.types.Texture) -> bool:
190+
target_material = getattr(self, material_attr)
191+
target_object = getattr(self, object_attr)
192+
if target_object is None:
193+
target_object = getattr(self, "id_data", None)
194+
195+
# must be a legal option... but is it a member of this material... or, if no material,
196+
# any of the materials attached to the object?
197+
if target_material is not None:
198+
result = value.name in target_material.texture_slots
199+
elif target_object is not None:
200+
for i in (slot.material for slot in target_object.material_slots if slot and slot.material):
201+
if value in (slot.texture for slot in i.texture_slots if slot and slot.texture):
202+
result = True
203+
break
204+
else:
205+
result = False
206+
else:
207+
result = False
208+
209+
# Is it animated?
210+
if result and target_material is not None:
211+
result = (
212+
(target_material.animation_data is not None and target_material.animation_data.action is not None)
213+
or (value.animation_data is not None and value.animation_data.action is not None)
214+
)
215+
216+
# Downstream processing, if any.
217+
if result and user_poll:
218+
result = user_poll(self, value)
219+
return result
220+
221+
assert not "type" in kwargs
222+
return PointerProperty(
223+
type=bpy.types.Texture,
224+
poll=poll_proc,
225+
**kwargs
226+
)
227+
148228
@bpy.app.handlers.persistent
149229
def _upgrade_node_trees(dummy):
150230
"""

korman/nodes/node_conditions.py

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
from PyHSPlasma import *
2222
from typing import *
2323

24+
from .. import enum_props
2425
from .node_core import *
25-
from ..properties.modifiers.physics import bounds_types
26+
from .node_deprecated import PlasmaVersionedNode
2627
from .. import idprops
2728

28-
class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
29+
class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
2930
bl_category = "CONDITIONS"
3031
bl_idname = "PlasmaClickableNode"
3132
bl_label = "Clickable"
@@ -38,10 +39,13 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
3839
description="Mesh object that is clickable",
3940
type=bpy.types.Object,
4041
poll=idprops.poll_mesh_objects)
41-
bounds = EnumProperty(name="Bounds",
42-
description="Clickable's bounds (NOTE: only used if your clickable is not a collider)",
43-
items=bounds_types,
44-
default="hull")
42+
43+
bounds = enum_props.bounds(
44+
"clickable_object",
45+
name="Bounds",
46+
description="Clickable's bounds",
47+
default="hull"
48+
)
4549

4650
input_sockets: dict[str, Any] = {
4751
"region": {
@@ -151,8 +155,20 @@ def requires_actor(self):
151155
def _idprop_mapping(cls):
152156
return {"clickable_object": "clickable"}
153157

158+
@property
159+
def latest_version(self):
160+
return 2
161+
162+
def upgrade(self):
163+
# In version 1 of this node, the bounds type was stored on this node. This could
164+
# be overridden by whatever was in the collision modifier. Version 2 changes the
165+
# bounds property to proxy to the collision modifier's bounds settings.
166+
if self.version == 2:
167+
enum_props.upgrade_bounds(self, "bounds")
168+
self.version = 2
154169

155-
class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
170+
171+
class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
156172
bl_category = "CONDITIONS"
157173
bl_idname = "PlasmaClickableRegionNode"
158174
bl_label = "Clickable Region Settings"
@@ -162,10 +178,12 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
162178
description="Object that defines the region mesh",
163179
type=bpy.types.Object,
164180
poll=idprops.poll_mesh_objects)
165-
bounds = EnumProperty(name="Bounds",
166-
description="Physical object's bounds (NOTE: only used if your clickable is not a collider)",
167-
items=bounds_types,
168-
default="hull")
181+
bounds = enum_props.bounds(
182+
"region_object",
183+
name="Bounds",
184+
description="Physical object's bounds",
185+
default="hull"
186+
)
169187

170188
output_sockets = {
171189
"satisfies": {
@@ -215,6 +233,18 @@ def convert_subcondition(self, exporter, parent_bo, parent_so, logicmod):
215233
def _idprop_mapping(cls):
216234
return {"region_object": "region"}
217235

236+
@property
237+
def latest_version(self):
238+
return 2
239+
240+
def upgrade(self):
241+
# In version 1 of this node, the bounds type was stored on this node. This could
242+
# be overridden by whatever was in the collision modifier. Version 2 changes the
243+
# bounds property to proxy to the collision modifier's bounds settings.
244+
if self.version == 1:
245+
enum_props.upgrade_bounds(self, "bounds")
246+
self.version = 2
247+
218248

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

397427

398-
class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
428+
class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
399429
bl_category = "CONDITIONS"
400430
bl_idname = "PlasmaVolumeSensorNode"
401431
bl_label = "Region Sensor"
@@ -419,9 +449,11 @@ def _update_report_on(self, context):
419449
description="Object that defines the region mesh",
420450
type=bpy.types.Object,
421451
poll=idprops.poll_mesh_objects)
422-
bounds = EnumProperty(name="Bounds",
423-
description="Physical object's bounds",
424-
items=bounds_types)
452+
bounds = enum_props.bounds(
453+
"region_object",
454+
name="Bounds",
455+
description="Physical object's bounds"
456+
)
425457

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

621+
@property
622+
def latest_version(self):
623+
return 2
624+
625+
def upgrade(self):
626+
# In version 1 of this node, the bounds type was stored on this node. This could
627+
# be overridden by whatever was in the collision modifier. Version 2 changes the
628+
# bounds property to proxy to the collision modifier's bounds settings.
629+
if self.version == 1:
630+
enum_props.upgrade_bounds(self, "bounds")
631+
self.version = 2
632+
589633

590634
class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase):
591635
bl_color = (43.1, 24.7, 0.0, 1.0)

0 commit comments

Comments
 (0)