#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "editor/EditorAuthority.h" #include "scene/editor/EditorSceneTarget.h" #include "editor/EditorTransactionService.h" #include using namespace eve::editor; namespace { EditorResult 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(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(rolledBack.code(), RuleId("test.scene.rollback"), "Could not roll back scene transaction"); return eve::editing::failed(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(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(operation.code()), static_cast(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(target.sceneObject(request.id).code()), static_cast(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(cycle.code()), static_cast(EditorStatus::Rejected)); CHECK_EQ(static_cast(hierarchy.planDelete(target, ObjectId("group")).code()), static_cast(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(target.sceneObject(ObjectId("leaf")).code()), static_cast(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(3)); CHECK_EQ(static_cast(provider.read(selection, PropertyPath("transform.scale")).state), static_cast(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(provider.read(selection, PropertyPath("transform.position")).state), static_cast(PropertyReadState::Mixed)); }