Files
EVEngine/test/editor_animation_graph.cpp
T
西风逍遥游andCursor 24502babef Nest satellite packages under their host modules.
Keep CMake module names and trim switches, but stop flattening editing/editor and runtime bridges as top-level src/modules directories. Parent scans skip nested facets so runtime-3d can drop authoring by directory leaf.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 17:56:12 +08:00

143 lines
6.1 KiB
C++

#include "animation/editing/AnimationGraph.h"
#include "animation/AnimClip.h"
#include "animation/AnimSkeleton.h"
#include "animation/AnimStateMachine.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <algorithm>
using namespace eve::animation_editing;
using namespace eve::editing;
namespace {
GraphDocumentData basicGraph(AnimationStateGraphDomain& domain) {
auto idle = domain.makeStateNode(GraphNodeId("idle"), "asset://animations/idle.eva");
auto run = domain.makeStateNode(GraphNodeId("run"), "asset://animations/run.eva");
auto transition = domain.makeTransitionNode(GraphNodeId("idle-to-run"));
REQUIRE(idle.ok());
REQUIRE(run.ok());
REQUIRE(transition.ok());
EditorValue::Object transitionProperties = *transition.value().properties.getIf<EditorValue::Object>();
transitionProperties["blendSeconds"] = 0.15;
transitionProperties["conditionKind"] = "float";
transitionProperties["parameter"] = "speed";
transitionProperties["operator"] = ">";
transitionProperties["threshold"] = 0.1;
transition.value().properties = EditorValue(std::move(transitionProperties));
eve::editing::GraphDocument graph;
REQUIRE(graph.createNode(idle.value()).ok());
REQUIRE(graph.createNode(run.value()).ok());
REQUIRE(graph.createNode(transition.value()).ok());
const GraphPinRecord* idleOut = graph.findPin(GraphPinId("idle.out"));
const GraphPinRecord* transitionIn = graph.findPin(GraphPinId("idle-to-run.in"));
const GraphPinRecord* transitionOut = graph.findPin(GraphPinId("idle-to-run.out"));
const GraphPinRecord* runIn = graph.findPin(GraphPinId("run.in"));
REQUIRE(idleOut);
REQUIRE(transitionIn);
REQUIRE(transitionOut);
REQUIRE(runIn);
REQUIRE(graph.connect({StableId("edge-in"), idleOut->id, transitionIn->id},
domain.canConnect(*idleOut, *transitionIn)).ok());
REQUIRE(graph.connect({StableId("edge-out"), transitionOut->id, runIn->id},
domain.canConnect(*transitionOut, *runIn)).ok());
EditorValue::Object parameters;
parameters["entry"] = "idle";
REQUIRE(graph.setParameters(EditorValue(std::move(parameters))).ok());
return graph.snapshot(domain.domain());
}
} // namespace
TEST_CASE("editor.animation.state_graph_compiles_stable_clip_references_and_conditions") {
AnimationStateGraphDomain domain;
const auto graph = basicGraph(domain);
const auto compiled = domain.compile(graph);
CHECK_EQ(static_cast<int>(compiled.status), static_cast<int>(EditorStatus::Applied));
CHECK_EQ(compiled.documentRevision, graph.revision);
CHECK(compiled.definition.find("EVANIM_STATE_GRAPH 1\n") == 0);
CHECK(compiled.definition.find("state \"idle\" \"asset://animations/idle.eva\"") != std::string::npos);
CHECK(compiled.definition.find("transition \"idle-to-run\" \"idle\" \"run\" 0.15") !=
std::string::npos);
CHECK(compiled.definition.find("\"float\" \"speed\" \">\" 0.1") != std::string::npos);
}
TEST_CASE("editor.animation.state_graph_rejects_incomplete_or_invalid_authoring_data") {
AnimationStateGraphDomain domain;
GraphDocumentData graph = basicGraph(domain);
graph.schemaVersion = 99;
CHECK_EQ(static_cast<int>(domain.compile(graph).status), static_cast<int>(EditorStatus::Unsupported));
graph = basicGraph(domain);
graph.edges.pop_back();
auto result = domain.compile(graph);
CHECK_EQ(static_cast<int>(result.status), static_cast<int>(EditorStatus::Failed));
REQUIRE(!result.diagnostics.empty());
graph = basicGraph(domain);
auto transition = std::find_if(graph.nodes.begin(), graph.nodes.end(), [](const GraphNodeRecord& node) {
return node.type == "animation.transition";
});
REQUIRE(transition != graph.nodes.end());
auto* properties = transition->properties.getIf<EditorValue::Object>();
REQUIRE(properties);
(*properties)["blendSeconds"] = -1.0;
result = domain.compile(graph);
CHECK_EQ(static_cast<int>(result.status), static_cast<int>(EditorStatus::Failed));
graph = basicGraph(domain);
auto* parameters = graph.parameters.getIf<EditorValue::Object>();
REQUIRE(parameters);
(*parameters)["entry"] = "missing";
result = domain.compile(graph);
CHECK_EQ(static_cast<int>(result.status), static_cast<int>(EditorStatus::Failed));
}
TEST_CASE("editor.animation.state_graph_rejects_direct_state_connections") {
AnimationStateGraphDomain domain;
auto idle = domain.makeStateNode(GraphNodeId("idle"), "idle.eva");
auto run = domain.makeStateNode(GraphNodeId("run"), "run.eva");
REQUIRE(idle.ok());
REQUIRE(run.ok());
CHECK(domain.canConnect(idle.value().pins[1], run.value().pins[0]).allowed);
GraphDocumentData invalid;
invalid.domain = domain.domain();
invalid.nodes = {idle.value(), run.value()};
invalid.edges = {{StableId("direct"), idle.value().pins[1].id, run.value().pins[0].id}};
invalid.parameters = EditorValue::Object{{"entry", "idle"}};
const auto compiled = domain.compile(invalid);
CHECK_EQ(static_cast<int>(compiled.status), static_cast<int>(EditorStatus::Failed));
}
TEST_CASE("editor.animation.state_graph_builds_real_runtime_state_machine") {
AnimationStateGraphDomain domain;
const GraphDocumentData graph = basicGraph(domain);
eve::animation::AnimSkeleton skeleton;
skeleton.addBone("root");
eve::animation::AnimClip idle("idle");
eve::animation::AnimClip run("run");
idle.setDuration(1.0F);
run.setDuration(1.0F);
AnimationStateGraphRuntimeBuilder builder;
auto built = builder.build(graph, &skeleton, [&](const std::string& asset) {
if (asset == "asset://animations/idle.eva") return &idle;
if (asset == "asset://animations/run.eva") return &run;
return static_cast<eve::animation::AnimClip*>(nullptr);
});
REQUIRE(built.ok());
CHECK_EQ(built.value()->getStateCount(), 2);
delete built.value();
auto missing = builder.build(graph, &skeleton, [](const std::string&) {
return static_cast<eve::animation::AnimClip*>(nullptr);
});
CHECK_EQ(static_cast<int>(missing.code()), static_cast<int>(EditorStatus::NotFound));
}