#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "editing/EditingValueJson.h" #include "scene/editing/SceneTarget.h" #include namespace { using namespace eve::editing; using namespace eve::scene_editing; struct Commands final : IEditingCommandRegistry { std::map planners; Result registerPlannedCommand(EditingCommandDescriptor descriptor, EditingCommandPlanner planner) override { planners.emplace(descriptor.id.value(), std::move(planner)); return applied(); } Result unregisterOwner(const std::string&) override { const auto count = planners.size(); planners.clear(); return applied(count); } Result plan(IEditableTarget& target, const std::string& command, Value payload) { CommandRequest request; request.payload = std::move(payload); return planners.at(command)(target, request); } }; TEST_CASE("scene.editing.commands_hierarchy_plans_are_reversible_and_backend_neutral") { Commands commands; REQUIRE(registerEditingCommands(commands).ok()); for (const auto* id : {"scene.object.create.v1", "scene.object.delete.v1", "scene.object.rename.v1", "scene.object.reparent.v1"}) REQUIRE(commands.planners.contains(id)); SceneDocumentTarget document("document"); RuntimeWorldTarget runtime("runtime"); for (SceneTargetBase* target : {static_cast(&document), static_cast(&runtime)}) { auto create = commands.plan(*target, "scene.object.create.v1", Value::Object{{"object", "cube"}, {"name", "Cube"}, {"position", Value::Array{1, 2, 3}}}); REQUIRE(create.ok()); REQUIRE(!target->sceneObject(ObjectId("cube")).ok()); REQUIRE_EQ(create.value().operations.size(), std::size_t(1)); REQUIRE(target->applyDomainOperation(create.value().operations.front()).ok()); REQUIRE_EQ(target->readTransform(ObjectId("cube")).value().y, 2.0); auto rename = commands.plan(*target, "scene.object.rename.v1", Value::Object{{"object", "cube"}, {"name", "Renamed"}}); REQUIRE(rename.ok()); auto op = rename.value().operations.front(); REQUIRE(target->applyDomainOperation(op).ok()); if (!op.inverseType.empty()) op.type = op.inverseType; op.payload = op.inverse; REQUIRE(target->applyDomainOperation(op).ok()); REQUIRE_EQ(target->sceneObject(ObjectId("cube")).value().name, std::string("Cube")); auto cycle = commands.plan(*target, "scene.object.reparent.v1", Value::Object{{"object", "cube"}, {"parent", "cube"}}); REQUIRE(!cycle.ok()); auto remove = commands.plan(*target, "scene.object.delete.v1", Value::Object{{"object", "cube"}}); REQUIRE(remove.ok()); REQUIRE(target->applyDomainOperation(remove.value().operations.front()).ok()); REQUIRE(target->sceneChildren({}).empty()); } } TEST_CASE("scene.editing.snapshot_restore_is_validated_atomic_and_reversible") { SceneDocumentTarget target("scene"); auto create = target.makeCreate({ObjectId("root"), {}, "Root", {}}); REQUIRE(create.ok()); REQUIRE(target.applyDomainOperation(create.value()).ok()); const auto before = target.snapshotValue(); auto decoded = editorValueFromJson(editorValueToJson(before)); REQUIRE(decoded.ok()); auto restore = target.makeRestore(decoded.value()); REQUIRE(restore.ok()); REQUIRE(target.applyDomainOperation(restore.value()).ok()); REQUIRE_EQ(editorValueToJson(before), editorValueToJson(target.snapshotValue())); auto invalid = decoded.value(); (*invalid.getIf())["schemaVersion"] = 99; const auto revision = target.revision(); REQUIRE(!target.makeRestore(invalid).ok()); REQUIRE_EQ(target.revision(), revision); invalid = decoded.value(); auto& objects = *invalid.getIf()->at("objects").getIf(); (*objects.front().getIf())["parent"] = "root"; REQUIRE(!target.makeRestore(invalid).ok()); REQUIRE_EQ(editorValueToJson(before), editorValueToJson(target.snapshotValue())); } } // namespace