diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 0728271..2e19b96 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -468,6 +468,12 @@ class Exporter: def _(temporary, parent): return handle_temporary(temporary.release(), parent) + @handle_temporary.register(bpy.types.Action) + def _(temporary, parent): + self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.actions.remove)) + log_msg(f"'{parent.name}': generated Action '{temporary.name}'") + return temporary + @handle_temporary.register(bpy.types.Object) def _(temporary, parent): self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.objects.remove)) diff --git a/korman/helpers.py b/korman/helpers.py index 59b5618..07690d4 100644 --- a/korman/helpers.py +++ b/korman/helpers.py @@ -17,6 +17,7 @@ import bmesh import bpy from contextlib import contextmanager import math +from typing import * @contextmanager def bmesh_from_object(bl): @@ -29,26 +30,17 @@ def bmesh_from_object(bl): finally: mesh.free() -@contextmanager -def duplicate_object(bl, remove_on_success=False): - with UiHelper(bpy.context): - for i in bpy.data.objects: - i.select = False - bl.select = True - bpy.context.scene.objects.active = bl - bpy.ops.object.duplicate() - dupe_object = bpy.context.active_object - - dupe_name = dupe_object.name - try: - yield dupe_object - except Exception: - if dupe_name in bpy.data.objects: - bpy.data.objects.remove(dupe_object) - raise - finally: - if remove_on_success and dupe_name in bpy.data.objects: - bpy.data.objects.remove(dupe_object) +def copy_action(source): + if source is not None and source.animation_data is not None and source.animation_data.action is not None: + source.animation_data.action = source.animation_data.action.copy() + return source.animation_data.action + +def copy_object(bl, name: Optional[str] = None): + dupe_object = bl.copy() + if name is not None: + dupe_object.name = name + bpy.context.scene.objects.link(dupe_object) + return dupe_object class GoodNeighbor: """Leave Things the Way You Found Them! (TM)""" diff --git a/korman/properties/modifiers/sound.py b/korman/properties/modifiers/sound.py index 63b174f..eff49b4 100644 --- a/korman/properties/modifiers/sound.py +++ b/korman/properties/modifiers/sound.py @@ -27,7 +27,7 @@ from ... import korlib from .base import PlasmaModifierProperties from .physics import surface_types from ...exporter import ExportError -from ...helpers import duplicate_object, GoodNeighbor, TemporaryCollectionItem +from ...helpers import copy_action, copy_object, GoodNeighbor, TemporaryCollectionItem from ... import idprops _randomsound_modes = { @@ -536,13 +536,14 @@ class PlasmaSoundEmitter(PlasmaModifierProperties): if self.have_3d_stereo and modifiers.random_sound.enabled: raise ExportError(f"{self.id_data.name}: Random Sound modifier cannot be applied to a Sound Emitter with 3D Stereo sounds.") - @contextmanager def _generate_stereized_emitter(self, exporter, bo: bpy.types.Object, channel: str, attr: str): # Duplicate the current sound emitter as a non-linked object so that we have all the # information that the parent emitter has, but we're free to turn off things as needed. - with duplicate_object(bo) as emitter_obj: + # NOTE: Don't immediately yield the copied object - we need to work on it first. If you immediately + # yield it, we get infinite recursion due to the copied object also having 3D Stereo sounds. + try: + emitter_obj = copy_object(bo, f"{bo.name}_Stereo-Ize:{channel}") emitter_obj.location = (0.0, 0.0, 0.0) - emitter_obj.name = f"{bo.name}_Stereo-Ize:{channel}" emitter_obj.parent = bo # In case some bozo is using a visual mesh as a sound emitter, clear the materials @@ -567,25 +568,23 @@ class PlasmaSoundEmitter(PlasmaModifierProperties): sound.enabled = False # And only sound volume animations! - if emitter_obj.animation_data is not None and emitter_obj.animation_data.action is not None: - action = emitter_obj.animation_data.action + emitter_obj_action = yield copy_action(emitter_obj) + if emitter_obj_action is not None: volume_paths = frozenset((i.path_from_id("volume") for i in soundemit_mod.sounds if i.enabled)) - toasty_fcurves = [i for i, fcurve in enumerate(action.fcurves) if fcurve.data_path not in volume_paths] + toasty_fcurves = [fcurve for fcurve in emitter_obj_action.fcurves if fcurve.data_path not in volume_paths] for i in reversed(toasty_fcurves): - action.fcurves.remove(i) + emitter_obj_action.fcurves.remove(i) # Again, only sound volume animations, which are handled above. - emitter_obj_data = emitter_obj.data - if emitter_obj_data is not None and emitter_obj_data.animation_data is not None and emitter_obj_data.animation_data.action is not None: - emitter_obj_data.animation_data.action.fcurves.clear() - - # Temporarily save a pointer to this generated emitter object so that the parent soundemit - # modifier can redirect requests to 3D sounds to the generated emitters. + emitter_data_action = yield copy_action(emitter_obj.data) + if emitter_data_action is not None: + emitter_data_action.fcurves.clear() + except Exception: + bpy.data.objects.remove(emitter_obj) + raise + else: + yield emitter_obj setattr(self, attr, emitter_obj) - try: - yield emitter_obj - finally: - self.property_unset(attr) def _find_animation_groups(self, bo: bpy.types.Object): is_anim_group = lambda x: ( @@ -616,8 +615,8 @@ class PlasmaSoundEmitter(PlasmaModifierProperties): # Find any animation groups that we're a part of - we need to be a member of those, # or create one *if* any animations - yield self._generate_stereized_emitter(exporter, bo, "L", "stereize_left") - yield self._generate_stereized_emitter(exporter, bo, "R", "stereize_right") + yield from self._generate_stereized_emitter(exporter, bo, "L", "stereize_left") + yield from self._generate_stereized_emitter(exporter, bo, "R", "stereize_right") # If some animation data persisted on the new emitters, then we need to make certain # that those animations are targetted by anyone trying to control us. That's an