advanced example

This commit is contained in:
Vysyr committed 2026-10-31 20:16:32 +01:00
1 parent 8c0a639fba
commit 6fe57915e8
99 files changed
+1329 -56

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
"""Automatic folder discovery, explicit selection and the manual reimport switch."""
import importlib.util
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import MagicMock, patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "Content/Python"))
from ue_workshop.characters import _clips
class Mesh:
def get_editor_property(self, name):
return "Skeleton"
class Clip:
def get_name(self):
return "EmbeddedTake"
class DiscoveryTests(unittest.TestCase):
def setUp(self):
self.folder = tempfile.TemporaryDirectory()
self.addCleanup(self.folder.cleanup)
self.model = Path(self.folder.name) / "Hero.fbx"
self.walk = self.model.with_name("great sword walk.fbx")
self.attack = self.model.with_name("great sword attack (2).fbx")
for path in (self.model, self.walk, self.attack):
path.touch()
unreal = MagicMock()
unreal.SkeletalMesh, unreal.AnimSequence = Mesh, Clip
name = "ue_workshop._skeletal_discovery_test"
path = Path(__file__).resolve().parents[1] / "Content/Python/ue_workshop/skeletal.py"
spec = importlib.util.spec_from_file_location(name, path)
self.module = importlib.util.module_from_spec(spec)
with patch.dict(sys.modules, {"unreal": unreal, name: self.module}):
spec.loader.exec_module(self.module)
self.cache = patch.object(self.module, "_cached", return_value=[Mesh()]).start()
self.importer = patch.object(self.module, "_import", return_value=[Mesh()]).start()
self.animation = patch.object(self.module, "import_animation", side_effect=lambda *a, **kw: Clip()).start()
self.addCleanup(patch.stopall)
def test_discovery_excludes_model_and_derives_names_without_aliases(self):
self.assertEqual(_clips(self.model), {
"great_sword_attack_2": self.attack,
"great_sword_walk": self.walk,
})
def test_omitted_animations_loads_every_neighbor_and_reuses_model(self):
result = self.module.import_model(self.model, name="Hero")
self.assertEqual(set(result.animations), {"great_sword_attack_2", "great_sword_walk"})
self.importer.assert_not_called()
self.assertEqual(self.animation.call_count, 2)
for call in self.animation.call_args_list:
self.assertFalse(call.kwargs["reimport"])
def test_empty_dictionary_disables_discovery_and_keeps_embedded_name(self):
self.cache.return_value = [Mesh(), Clip()]
result = self.module.import_model(self.model, name="Hero", animations={})
self.assertEqual(set(result.animations), {"EmbeddedTake"})
self.animation.assert_not_called()
def test_explicit_selection_ignores_other_files(self):
result = self.module.import_model(self.model, name="Hero", animations={"selected": self.walk})
self.assertEqual(set(result.animations), {"selected"})
self.animation.assert_called_once()
self.assertEqual(self.animation.call_args.args[0], self.walk)
def test_reimport_true_reimports_model_and_discovered_clips(self):
self.module.import_model(self.model, name="Hero", reimport=True)
self.cache.assert_not_called()
self.importer.assert_called_once()
self.assertTrue(self.importer.call_args.kwargs["reimport"])
self.assertTrue(all(call.kwargs["reimport"] for call in self.animation.call_args_list))
def test_colliding_normalized_names_are_rejected(self):
self.model.with_name("great-sword-walk.fbx").touch()
with self.assertRaises(ValueError):
_clips(self.model)
+145
View File
@@ -0,0 +1,145 @@
"""Validate clip scheduling without an Unreal installation."""
import importlib.util
from pathlib import Path
import sys
from types import SimpleNamespace
import unittest
from unittest.mock import MagicMock, patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "Content/Python"))
class Clip:
def __init__(self, skeleton):
self.skeleton = skeleton
def get_editor_property(self, name):
assert name == "skeleton"
return self.skeleton
class AnimationTests(unittest.TestCase):
def setUp(self):
unreal = MagicMock()
unreal.AnimSequence = Clip
unreal.FrameNumber.side_effect = lambda frame: frame
path = Path(__file__).resolve().parents[1] / "Content/Python/ue_workshop/sequence.py"
spec = importlib.util.spec_from_file_location("ue_workshop._sequence_test", path)
module = importlib.util.module_from_spec(spec)
with patch.dict(sys.modules, {"unreal": unreal}):
spec.loader.exec_module(module)
self.unreal = unreal
self.shot = module.Sequence.__new__(module.Sequence)
self.shot.frames = 120
self.shot.fps = 24
self.shot.asset = MagicMock()
self.shot.asset.get_tick_resolution.return_value = SimpleNamespace(numerator=24000, denominator=1)
self.shot._animations = {}
self.shot.bind = MagicMock()
self.actor = MagicMock()
self.actor.get_path_name.return_value = "Parent"
self.child = MagicMock()
self.child.get_path_name.return_value = "SkeletalChild"
self.skeleton = object()
self.clip = Clip(self.skeleton)
self.imported = SimpleNamespace(mesh=MagicMock(), animations={"walk": self.clip})
self.imported.mesh.get_editor_property.return_value = self.skeleton
self.shot.scene = SimpleNamespace(_skeletal_models={"Parent": (self.child, self.imported)})
def test_adjacent_clips_share_child_track_and_set_speed(self):
self.shot.animation(self.actor, "walk", start=0, end=48)
self.shot.animation(self.actor, "walk", start=48, end=120, speed=0.8)
self.shot.bind.assert_called_once_with(self.child)
track = self.shot.bind.return_value.add_track.return_value
self.assertEqual(track.add_section.call_count, 2)
track.add_section.return_value.set_range.assert_called_with(48, 120)
self.unreal.MovieSceneTimeWarpVariant.return_value.set_fixed_play_rate.assert_called_with(0.8)
def test_nested_clip_is_rejected_without_creating_section(self):
self.shot.animation(self.actor, "walk", start=0, end=48)
with self.assertRaises(ValueError):
self.shot.animation(self.actor, "walk", start=12, end=36)
self.assertEqual(self.shot.bind.return_value.add_track.return_value.add_section.call_count, 1)
def test_invalid_ranges_and_speeds_do_not_create_track(self):
for options in ({"start": -1}, {"end": 121}, {"start": 48, "end": 48},
{"start": True}, {"speed": 0}, {"speed": float("nan")},
{"speed": float("inf")}):
with self.subTest(options=options), self.assertRaises(ValueError):
self.shot.animation(self.actor, "walk", **options)
self.shot.bind.assert_not_called()
def test_crossfade_sets_complementary_weight_curves(self):
track = self.shot.bind.return_value.add_track.return_value
first, second = MagicMock(), MagicMock()
track.add_section.side_effect = [first, second]
self.shot.animation(self.actor, "walk", start=0, end=60)
self.shot.animation(self.actor, "walk", start=48, end=120)
outgoing = first.get_all_channels.return_value[0].add_key
incoming = second.get_all_channels.return_value[0].add_key
outgoing.assert_any_call(48, 1.0, interpolation=self.unreal.MovieSceneKeyInterpolation.LINEAR)
outgoing.assert_any_call(60, 0.0, interpolation=self.unreal.MovieSceneKeyInterpolation.LINEAR)
incoming.assert_any_call(48, 0.0, interpolation=self.unreal.MovieSceneKeyInterpolation.LINEAR)
incoming.assert_any_call(60, 1.0, interpolation=self.unreal.MovieSceneKeyInterpolation.LINEAR)
self.shot.bind.assert_called_once_with(self.child)
def test_ambiguous_overlaps_are_rejected_before_creating_section(self):
self.shot.animation(self.actor, start=0, end=60)
for start, end in ((0, 60), (0, 120), (12, 36), (30, 60)):
with self.subTest(start=start, end=end), self.assertRaises(ValueError):
self.shot.animation(self.actor, start=start, end=end)
self.assertEqual(self.shot.bind.return_value.add_track.return_value.add_section.call_count, 1)
def test_three_clip_chain_preserves_middle_fade_in_and_adds_fade_out(self):
track = self.shot.bind.return_value.add_track.return_value
first, middle, last = MagicMock(), MagicMock(), MagicMock()
track.add_section.side_effect = [first, middle, last]
self.shot.animation(self.actor, start=0, end=48)
self.shot.animation(self.actor, start=36, end=84)
self.shot.animation(self.actor, start=72, end=120)
weight = middle.get_all_channels.return_value[0].add_key
weight.assert_any_call(36, 0.0, interpolation=self.unreal.MovieSceneKeyInterpolation.LINEAR)
weight.assert_any_call(48, 1.0, interpolation=self.unreal.MovieSceneKeyInterpolation.LINEAR)
weight.assert_any_call(72, 1.0, interpolation=self.unreal.MovieSceneKeyInterpolation.LINEAR)
weight.assert_any_call(84, 0.0, interpolation=self.unreal.MovieSceneKeyInterpolation.LINEAR)
def test_triple_overlap_is_rejected(self):
self.shot.animation(self.actor, start=0, end=60)
self.shot.animation(self.actor, start=48, end=100)
with self.assertRaises(ValueError):
self.shot.animation(self.actor, start=54, end=120)
def test_single_frame_overlap_automatically_blends(self):
self.shot.animation(self.actor, start=0, end=60)
self.shot.animation(self.actor, start=59, end=120)
self.assertEqual(self.shot._animations["SkeletalChild"][1][4], 1)
def test_adjacent_clips_do_not_fade(self):
self.shot.animation(self.actor, start=0, end=60)
self.shot.animation(self.actor, start=60, end=120)
weight = self.shot.bind.return_value.add_track.return_value.add_section.return_value.get_all_channels.return_value[0]
weight.add_key.assert_not_called()
def test_out_of_order_overlap_is_rejected(self):
self.shot.animation(self.actor, start=30, end=90)
with self.assertRaises(ValueError):
self.shot.animation(self.actor, start=0, end=60)
def test_unknown_clip_and_foreign_skeleton_are_rejected(self):
with self.assertRaises(ValueError):
self.shot.animation(self.actor, "missing")
with self.assertRaises(ValueError):
self.shot.animation(self.actor, Clip(object()))
self.shot.bind.assert_not_called()
def test_default_requires_exactly_one_clip(self):
self.shot.animation(self.actor)
self.imported.animations["idle"] = Clip(self.skeleton)
with self.assertRaises(ValueError):
self.shot.animation(self.actor)
def test_static_actor_cannot_receive_skeletal_animation(self):
self.shot.scene._skeletal_models.clear()
with self.assertRaises(ValueError):
self.shot.animation(self.actor, "walk")
self.shot.bind.assert_not_called()