Files
EVEngine/test/editor_scene_target_v2.cpp
西风逍遥游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

249 lines
12 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "editor/EditorAuthority.h"
#include "scene/editor/EditorSceneTarget.h"
#include "editor/EditorTransactionService.h"
#include <string>
using namespace eve::editor;
namespace {
EditorResult<TransactionReceipt> commitOperation(SceneTargetBase& target, LocalTransactionBackend& transactions,
const DomainOperation& operation, const std::string& id) {
TransactionSpec specification;
specification.id = TransactionId(id);
specification.label = "Scene edit";
specification.target = TargetId(target.targetId());
specification.baseRevision = target.revision();
auto begun = transactions.begin(std::move(specification));
if (!begun.ok())
return eve::editing::failed<TransactionReceipt>(begun.code(), RuleId("test.scene.begin"),
"Could not begin scene transaction");
auto appended = transactions.append(operation);
if (!appended.ok()) {
auto rolledBack = transactions.rollback();
if (!rolledBack.ok())
return eve::editing::failed<TransactionReceipt>(rolledBack.code(), RuleId("test.scene.rollback"),
"Could not roll back scene transaction");
return eve::editing::failed<TransactionReceipt>(appended.code(), RuleId("test.scene.append"),
"Could not append scene operation");
}
return transactions.commit();
}
void exerciseSceneBackend(SceneTargetBase& target, const std::string& transactionPrefix) {
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
ScenePlacementToolLogic placement;
SceneTransformToolLogic transform;
CreateSceneObjectRequest root;
root.id = ObjectId("root");
root.name = "Root";
root.transform = {0.0, 0.0, 0.0};
auto rootOperation = placement.plan(target, root);
CHECK(rootOperation.ok());
CHECK(commitOperation(target, transactions, rootOperation.value(), transactionPrefix + ".root").ok());
CreateSceneObjectRequest child;
child.id = ObjectId("coaster");
child.parent = root.id;
child.name = "Coaster";
child.transform = {1.0, 0.0, 2.0};
auto childOperation = placement.plan(target, child);
CHECK(childOperation.ok());
CHECK(commitOperation(target, transactions, childOperation.value(), transactionPrefix + ".child").ok());
CHECK_EQ(target.sceneChildren(root.id).size(), static_cast<std::size_t>(1));
SceneTransformValue moved{8.0, 3.0, 5.0, 0.1, 0.5, -0.2, 2.0, 1.5, 0.75};
auto transformOperation = transform.plan(target, child.id, moved);
CHECK(transformOperation.ok());
CHECK(commitOperation(target, transactions, transformOperation.value(), transactionPrefix + ".move").ok());
CHECK(target.readTransform(child.id).value() == moved);
CHECK(transactions.undo().ok());
CHECK(target.readTransform(child.id).value() == child.transform);
CHECK(transactions.redo().ok());
CHECK(target.readTransform(child.id).value() == moved);
}
TEST_CASE("editor.v2.scene_transform_supports_atomic_multi_object_trs_edits") {
RuntimeWorldTarget target("runtime");
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
ScenePlacementToolLogic placement;
SceneTransformToolLogic transform;
for (const std::string id : {"left", "right"}) {
CreateSceneObjectRequest request;
request.id = ObjectId(id);
request.name = id;
auto operation = placement.plan(target, request);
REQUIRE(operation.ok());
REQUIRE(commitOperation(target, transactions, operation.value(), "create." + id).ok());
}
const SceneTransformValue leftValue{1.0, 2.0, 3.0, 0.0, 0.25, 0.0, 2.0, 2.0, 2.0};
const SceneTransformValue rightValue{-1.0, 4.0, 6.0, 0.5, 0.0, 1.0, 0.5, 1.0, 1.5};
auto left = transform.plan(target, ObjectId("left"), leftValue);
auto right = transform.plan(target, ObjectId("right"), rightValue);
REQUIRE(left.ok());
REQUIRE(right.ok());
TransactionSpec specification;
specification.id = TransactionId("transform.multi");
specification.label = "Transform selection";
specification.target = TargetId(target.targetId());
specification.baseRevision = target.revision();
REQUIRE(transactions.begin(std::move(specification)).ok());
REQUIRE(transactions.append(left.value()).ok());
REQUIRE(transactions.append(right.value()).ok());
REQUIRE(transactions.commit().ok());
CHECK(target.readTransform(ObjectId("left")).value() == leftValue);
CHECK(target.readTransform(ObjectId("right")).value() == rightValue);
REQUIRE(transactions.undo().ok());
CHECK(target.readTransform(ObjectId("left")).value() == SceneTransformValue{});
CHECK(target.readTransform(ObjectId("right")).value() == SceneTransformValue{});
REQUIRE(transactions.redo().ok());
CHECK(target.readTransform(ObjectId("left")).value() == leftValue);
CHECK(target.readTransform(ObjectId("right")).value() == rightValue);
}
} // namespace
TEST_CASE("editor.v2.scene_document_and_runtime_targets_are_tool_isomorphic") {
SceneDocumentTarget document("document-scene");
RuntimeWorldTarget runtime("runtime-scene");
exerciseSceneBackend(document, "document");
exerciseSceneBackend(runtime, "runtime");
CHECK_EQ(document.describe().type, std::string("scene-document"));
CHECK_EQ(runtime.describe().type, std::string("runtime-world"));
CHECK_EQ(document.describe().capabilities, runtime.describe().capabilities);
}
TEST_CASE("editor.v2.scene_targets_expose_stable_capability_ids") {
SceneDocumentTarget target("scene");
CHECK(target.queryCapability(ISceneHierarchyEditTarget::editorCapabilityId()) != nullptr);
CHECK(target.queryCapability(ITransformEditTarget::editorCapabilityId()) != nullptr);
CHECK(target.queryCapability(CapabilityId("unknown")) == nullptr);
CreateSceneObjectRequest invalid;
invalid.id = ObjectId("child");
invalid.parent = ObjectId("missing");
auto operation = ScenePlacementToolLogic().plan(target, invalid);
CHECK_EQ(static_cast<int>(operation.code()), static_cast<int>(EditorStatus::NotFound));
}
TEST_CASE("editor.v2.scene_create_is_reversible_through_authority") {
RuntimeWorldTarget target("runtime");
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
CreateSceneObjectRequest request;
request.id = ObjectId("placed-object");
request.name = "Placed Object";
auto operation = ScenePlacementToolLogic().plan(target, request);
CHECK(operation.ok());
CHECK(commitOperation(target, transactions, operation.value(), "place").ok());
CHECK(target.sceneObject(request.id).ok());
CHECK(transactions.undo().ok());
CHECK_EQ(static_cast<int>(target.sceneObject(request.id).code()), static_cast<int>(EditorStatus::NotFound));
}
TEST_CASE("editor.v2.scene_hierarchy_edits_are_reversible_and_cycle_safe") {
SceneDocumentTarget target("scene");
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
ScenePlacementToolLogic placement;
SceneHierarchyToolLogic hierarchy;
for (const auto& request : {CreateSceneObjectRequest{ObjectId("root"), {}, "Root", {}},
CreateSceneObjectRequest{ObjectId("group"), ObjectId("root"), "Group", {}},
CreateSceneObjectRequest{ObjectId("leaf"), ObjectId("group"), "Leaf", {}}}) {
auto operation = placement.plan(target, request);
REQUIRE(operation.ok());
REQUIRE(commitOperation(target, transactions, operation.value(), "create." + request.id.value()).ok());
}
auto cycle = hierarchy.planReparent(target, ObjectId("root"), ObjectId("leaf"));
CHECK_EQ(static_cast<int>(cycle.code()), static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(static_cast<int>(hierarchy.planDelete(target, ObjectId("group")).code()),
static_cast<int>(EditorStatus::Rejected));
auto rename = hierarchy.planRename(target, ObjectId("leaf"), "Camera");
REQUIRE(rename.ok());
REQUIRE(commitOperation(target, transactions, rename.value(), "rename").ok());
CHECK_EQ(target.sceneObject(ObjectId("leaf")).value().name, std::string("Camera"));
REQUIRE(transactions.undo().ok());
CHECK_EQ(target.sceneObject(ObjectId("leaf")).value().name, std::string("Leaf"));
REQUIRE(transactions.redo().ok());
auto reparent = hierarchy.planReparent(target, ObjectId("leaf"), ObjectId("root"));
REQUIRE(reparent.ok());
REQUIRE(commitOperation(target, transactions, reparent.value(), "reparent").ok());
CHECK(target.sceneObject(ObjectId("leaf")).value().parent == ObjectId("root"));
auto remove = hierarchy.planDelete(target, ObjectId("leaf"));
REQUIRE(remove.ok());
REQUIRE(commitOperation(target, transactions, remove.value(), "delete").ok());
CHECK_EQ(static_cast<int>(target.sceneObject(ObjectId("leaf")).code()),
static_cast<int>(EditorStatus::NotFound));
REQUIRE(transactions.undo().ok());
CHECK_EQ(target.sceneObject(ObjectId("leaf")).value().name, std::string("Camera"));
CHECK(target.sceneObject(ObjectId("leaf")).value().parent == ObjectId("root"));
}
TEST_CASE("editor.v2.scene_property_provider_multi_edits_generic_trs_inspector") {
SceneDocumentTarget target("scene");
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
ScenePlacementToolLogic placement;
for (const std::string id : {"one", "two"}) {
CreateSceneObjectRequest request;
request.id = ObjectId(id);
request.name = id;
auto operation = placement.plan(target, request);
REQUIRE(operation.ok());
REQUIRE(commitOperation(target, transactions, operation.value(), "property.create." + id).ok());
}
SelectionSnapshot selection;
selection.channel = "scene";
for (const std::string id : {"one", "two"}) {
SelectionItem item;
item.domain = SelectionDomain::Scene;
item.target = TargetId(target.targetId());
item.item = StableId(id);
item.type = "scene.object";
selection.items.push_back(item);
}
selection.primary = selection.items.front();
ScenePropertyProvider provider(&target);
CHECK_EQ(provider.schema(selection).properties.size(), static_cast<std::size_t>(3));
CHECK_EQ(static_cast<int>(provider.read(selection, PropertyPath("transform.scale")).state),
static_cast<int>(PropertyReadState::Value));
auto setScale = provider.makeSet(selection, PropertyPath("transform.scale"),
EditorValue::Array{2.0, 3.0, 4.0}, PropertySetMode::Absolute);
REQUIRE(setScale.ok());
REQUIRE(commitOperation(target, transactions, setScale.value(), "property.scale").ok());
CHECK_EQ(target.readTransform(ObjectId("one")).value().scaleY, 3.0);
CHECK_EQ(target.readTransform(ObjectId("two")).value().scaleZ, 4.0);
REQUIRE(transactions.undo().ok());
CHECK_EQ(target.readTransform(ObjectId("one")).value().scaleY, 1.0);
CHECK_EQ(target.readTransform(ObjectId("two")).value().scaleZ, 1.0);
SceneTransformToolLogic transform;
SceneTransformValue distinct;
distinct.x = 9.0;
auto moveOne = transform.plan(target, ObjectId("one"), distinct);
REQUIRE(moveOne.ok());
REQUIRE(commitOperation(target, transactions, moveOne.value(), "property.mixed").ok());
CHECK_EQ(static_cast<int>(provider.read(selection, PropertyPath("transform.position")).state),
static_cast<int>(PropertyReadState::Mixed));
}