142 lines
7.1 KiB
C++
142 lines
7.1 KiB
C++
#include "action/editor/ActionTimelinePayloadEditor.h"
|
|
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
namespace {
|
|
|
|
class FixedVfxDurationProvider final : public eve::action::IActionVfxDurationProvider {
|
|
public:
|
|
eve::Result<eve::Duration> naturalDuration(std::string_view uri) const override {
|
|
lastUri = std::string(uri);
|
|
return eve::Result<eve::Duration>::success(eve::Duration::fromSeconds(1.8).value());
|
|
}
|
|
|
|
mutable std::string lastUri;
|
|
};
|
|
|
|
eve::LogicalId payloadId(const char* value) {
|
|
auto parsed = eve::LogicalId::parse(value);
|
|
REQUIRE(parsed.has_value());
|
|
return std::move(*parsed);
|
|
}
|
|
|
|
eve::action::ActionTimeline payloadTimeline() {
|
|
eve::action::ActionTimeline timeline;
|
|
timeline.actionId = payloadId("test:payload-editor");
|
|
timeline.duration = eve::Duration::fromNanoseconds(100);
|
|
eve::action::ActionTrack track;
|
|
track.id = payloadId("test-track:audio");
|
|
track.label = "Audio";
|
|
track.kind = eve::action::ActionTrackKind::Audio;
|
|
track.states.push_back({payloadId("test-state:audio"), payloadId("presentation:audio-state"),
|
|
eve::Duration::fromNanoseconds(10), eve::Duration::fromNanoseconds(50),
|
|
{{"uri", "asset://audio/swing.wav"}, {"extensionField", "preserved"}}});
|
|
timeline.tracks.push_back(std::move(track));
|
|
return timeline;
|
|
}
|
|
|
|
eve::action::ActionTimeline vfxPayloadTimeline() {
|
|
eve::action::ActionTimeline timeline;
|
|
timeline.actionId = payloadId("test:vfx-clip-fit");
|
|
timeline.duration = eve::Duration::fromSeconds(5.0).value();
|
|
eve::action::ActionTrack track;
|
|
track.id = payloadId("test-track:vfx");
|
|
track.label = "VFX";
|
|
track.kind = eve::action::ActionTrackKind::Effect;
|
|
track.states.push_back({payloadId("test-state:vfx"), payloadId("presentation:vfx-state"),
|
|
eve::Duration::fromSeconds(1.0).value(), eve::Duration::fromSeconds(1.5).value(),
|
|
{{"uri", "asset://vfx/slash.json"}, {"clipStartTime", 0.25},
|
|
{"clipEndTime", 1.25}, {"extensionField", "preserved"}}});
|
|
timeline.tracks.push_back(std::move(track));
|
|
return timeline;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("actionTimelinePayloadEditor.patchPreservesUnknownFieldsAndRejectsAtomically") {
|
|
eve::editor::ActionTimelineEditor editor("test.payload-editor", payloadTimeline());
|
|
auto registry = eve::action::ActionNotifyRegistry::withBuiltins();
|
|
REQUIRE(registry.ok());
|
|
eve::editor::ActionTimelinePayloadEditor payloads(editor, registry.value());
|
|
const auto itemId = payloadId("test-state:audio");
|
|
|
|
REQUIRE(payloads.patch(itemId, {{"volume", 0.5}, {"looping", true},
|
|
{"randomUris", eve::Value::Array{"asset://audio/a.wav",
|
|
"asset://audio/b.wav"}},
|
|
{"randomPitchOffset", 0.08}, {"spatialBlend", 0.5},
|
|
{"minDistance", 2.0}, {"maxDistance", 24.0},
|
|
{"fadeOutOnExit", true}, {"fadeOutDuration", 0.2}}).ok());
|
|
auto edited = payloads.payload(itemId);
|
|
REQUIRE(edited.ok());
|
|
CHECK_EQ(*edited.value().at("extensionField").getIf<std::string>(), "preserved");
|
|
CHECK_EQ(*edited.value().at("volume").getIf<double>(), 0.5);
|
|
CHECK(*edited.value().at("looping").getIf<bool>());
|
|
CHECK_EQ(edited.value().at("randomUris").getIf<eve::Value::Array>()->size(), 2U);
|
|
|
|
const auto revision = editor.target().revision();
|
|
CHECK(!payloads.patch(itemId, {{"pitch", 0.0}}).ok());
|
|
CHECK_EQ(editor.target().revision(), revision);
|
|
auto unchanged = payloads.payload(itemId);
|
|
REQUIRE(unchanged.ok());
|
|
CHECK(!unchanged.value().contains("pitch"));
|
|
CHECK_EQ(*unchanged.value().at("maxDistance").getIf<double>(), 24.0);
|
|
REQUIRE(editor.undo().ok());
|
|
CHECK_EQ(*payloads.payload(itemId).value().at("extensionField").getIf<std::string>(), "preserved");
|
|
}
|
|
|
|
TEST_CASE("actionTimelinePayloadEditor.vfxClipFitCommandsAreAtomicAndUndoable") {
|
|
eve::editor::ActionTimelineEditor editor("test.vfx-clip-fit", vfxPayloadTimeline());
|
|
auto registry = eve::action::ActionNotifyRegistry::withBuiltins();
|
|
REQUIRE(registry.ok());
|
|
eve::editor::ActionTimelinePayloadEditor payloads(editor, registry.value());
|
|
const auto itemId = payloadId("test-state:vfx");
|
|
|
|
REQUIRE(payloads.fitBlockToClip(itemId).ok());
|
|
const auto& fittedBlock = editor.target().timeline().tracks.front().states.front();
|
|
CHECK_EQ(fittedBlock.start.seconds(), 1.0);
|
|
CHECK_EQ(fittedBlock.end.seconds(), 2.0);
|
|
CHECK_EQ(editor.target().revision(), 1ULL);
|
|
|
|
REQUIRE(editor.undo().ok());
|
|
CHECK_EQ(editor.target().timeline().tracks.front().states.front().end.seconds(), 1.5);
|
|
REQUIRE(payloads.fitClipToBlock(itemId).ok());
|
|
auto fittedClip = payloads.payload(itemId);
|
|
REQUIRE(fittedClip.ok());
|
|
CHECK_EQ(*fittedClip.value().at("clipStartTime").getIf<double>(), 0.25);
|
|
CHECK_EQ(*fittedClip.value().at("clipEndTime").getIf<double>(), 0.75);
|
|
CHECK_EQ(*fittedClip.value().at("extensionField").getIf<std::string>(), "preserved");
|
|
REQUIRE(editor.undo().ok());
|
|
CHECK_EQ(*payloads.payload(itemId).value().at("clipEndTime").getIf<double>(), 1.25);
|
|
|
|
FixedVfxDurationProvider provider;
|
|
REQUIRE(payloads.fitClipToNaturalDuration(itemId, std::cref(provider)).ok());
|
|
auto naturalClip = payloads.payload(itemId);
|
|
REQUIRE(naturalClip.ok());
|
|
CHECK_EQ(provider.lastUri, std::string("asset://vfx/slash.json"));
|
|
CHECK_EQ(*naturalClip.value().at("clipEndTime").getIf<double>(), 1.8);
|
|
CHECK_EQ(*naturalClip.value().at("extensionField").getIf<std::string>(), "preserved");
|
|
REQUIRE(editor.undo().ok());
|
|
CHECK_EQ(*payloads.payload(itemId).value().at("clipEndTime").getIf<double>(), 1.25);
|
|
}
|
|
|
|
TEST_CASE("actionTimelinePayloadEditor.vfxClipFitRejectsWrongShapeAndOutOfRangeWithoutMutation") {
|
|
auto timeline = vfxPayloadTimeline();
|
|
timeline.tracks.front().notifies.push_back(
|
|
{payloadId("test-notify:vfx"), payloadId("presentation:vfx"), eve::Duration::fromSeconds(0.25).value(),
|
|
{{"uri", "asset://vfx/slash.json"}, {"lifetimeSeconds", 1.0}}});
|
|
timeline.tracks.front().states.front().start = eve::Duration::fromSeconds(4.5).value();
|
|
timeline.tracks.front().states.front().end = eve::Duration::fromSeconds(4.75).value();
|
|
eve::editor::ActionTimelineEditor editor("test.vfx-clip-fit-reject", std::move(timeline));
|
|
auto registry = eve::action::ActionNotifyRegistry::withBuiltins();
|
|
REQUIRE(registry.ok());
|
|
eve::editor::ActionTimelinePayloadEditor payloads(editor, registry.value());
|
|
|
|
const auto revision = editor.target().revision();
|
|
CHECK(!payloads.fitBlockToClip(payloadId("test-state:vfx")).ok());
|
|
CHECK(!payloads.fitClipToBlock(payloadId("test-notify:vfx")).ok());
|
|
CHECK(!payloads.fitClipToNaturalDuration(payloadId("test-state:vfx"), std::nullopt).ok());
|
|
CHECK_EQ(editor.target().revision(), revision);
|
|
CHECK_EQ(editor.target().timeline().tracks.front().states.front().end.seconds(), 4.75);
|
|
}
|