#include "editor/EditorAuthority.h" #include "physics/editor/EditorPhysicsTarget.h" #include "editor/EditorTransactionService.h" #include "zeroerr/assert.h" #include "zeroerr/unittest.h" using namespace eve::editor; namespace { SelectionSnapshot colliderSelection(const PhysicsColliderTarget& target) { SelectionSnapshot selection; selection.channel = "scene"; SelectionItem item; item.domain = SelectionDomain::Scene; item.target = TargetId(target.targetId()); item.item = StableId(target.targetId().value()); item.type = target.describe().type; selection.items.push_back(item); selection.primary = item; return selection; } SelectionSnapshot jointSelection(const PhysicsJointTarget& target) { SelectionSnapshot selection; selection.channel = "scene"; SelectionItem item; item.domain = SelectionDomain::Scene; item.target = TargetId(target.targetId()); item.item = StableId(target.targetId().value()); item.type = target.describe().type; selection.items.push_back(item); selection.primary = item; return selection; } EditorResult commitCollider(PhysicsColliderTarget& target, LocalTransactionBackend& transactions, const DomainOperation& operation, const char* id) { TransactionSpec specification; specification.id = TransactionId(id); specification.label = "Edit collider"; 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.physics.begin"), "Could not begin collider transaction"); auto appended = transactions.append(operation); if (!appended.ok()) { [[maybe_unused]] const auto rolledBack = transactions.rollback(); return eve::editing::failed(appended.code(), RuleId("test.physics.append"), "Could not append collider operation"); } return transactions.commit(); } } // namespace TEST_CASE("editor.physics.collider_schema_tracks_backend_dimensionality") { PhysicsColliderTarget target2d("fixture", 2); PhysicsColliderTarget target3d("shape", 3); const auto schema2d = target2d.schema(colliderSelection(target2d)); const auto schema3d = target3d.schema(colliderSelection(target3d)); CHECK_EQ(schema2d.typeId, std::string("physics.collider2d")); CHECK_EQ(schema3d.typeId, std::string("physics.collider3d")); REQUIRE(schema2d.find(PropertyPath("shape.kind"))); REQUIRE(schema3d.find(PropertyPath("shape.kind"))); CHECK_EQ(schema2d.find(PropertyPath("shape.kind"))->enumItems.size(), static_cast(4)); CHECK_EQ(schema3d.find(PropertyPath("shape.kind"))->enumItems.size(), static_cast(6)); CHECK_EQ(target2d.describe().type, std::string("physics-collider-2d")); CHECK_EQ(target3d.describe().type, std::string("physics-collider-3d")); } TEST_CASE("editor.physics.collider_edits_validate_and_undo") { PhysicsColliderTarget target("shape", 3); LocalWorldAuthority authority(&target); LocalTransactionBackend transactions(&authority); const SelectionSnapshot selection = colliderSelection(target); auto invalid = target.makeSet(selection, PropertyPath("shape.size"), EditorValue::Array{1.0, -2.0, 3.0}, PropertySetMode::Absolute); CHECK_EQ(static_cast(invalid.code()), static_cast(EditorStatus::Rejected)); invalid = target.makeSet(selection, PropertyPath("material.friction"), 1.5, PropertySetMode::Absolute); CHECK_EQ(static_cast(invalid.code()), static_cast(EditorStatus::Rejected)); auto shape = target.makeSet(selection, PropertyPath("shape.kind"), "capsule", PropertySetMode::Absolute); REQUIRE(shape.ok()); REQUIRE(commitCollider(target, transactions, shape.value(), "collider.shape").ok()); auto radius = target.makeSet(selection, PropertyPath("shape.radius"), 0.75, PropertySetMode::Absolute); REQUIRE(radius.ok()); REQUIRE(commitCollider(target, transactions, radius.value(), "collider.radius").ok()); CHECK(target.read(selection, PropertyPath("shape.radius")).value == EditorValue(0.75)); REQUIRE(transactions.undo().ok()); CHECK(target.read(selection, PropertyPath("shape.radius")).value == EditorValue(0.5)); REQUIRE(transactions.redo().ok()); CHECK_EQ(target.validate().size(), static_cast(1)); } TEST_CASE("editor.physics.collider_snapshot_is_versioned_atomic_and_diagnostic") { PhysicsColliderTarget source("source", 3); const SelectionSnapshot selection = colliderSelection(source); auto mesh = source.makeSet(selection, PropertyPath("shape.kind"), "triangle-mesh", PropertySetMode::Absolute); REQUIRE(mesh.ok()); REQUIRE(source.applyDomainOperation(mesh.value()).ok()); CHECK_EQ(source.validate().size(), static_cast(1)); PhysicsColliderTarget restored("restored", 3); REQUIRE(restored.loadSnapshot(source.snapshotValue()).ok()); CHECK(restored.read(colliderSelection(restored), PropertyPath("shape.kind")).value == EditorValue("triangle-mesh")); PhysicsColliderTarget wrongDimension("wrong", 2); CHECK_EQ(static_cast(wrongDimension.loadSnapshot(source.snapshotValue()).code()), static_cast(EditorStatus::Rejected)); CHECK(wrongDimension.read(colliderSelection(wrongDimension), PropertyPath("shape.kind")).value == EditorValue("box")); EditorValue unknownVersion = source.snapshotValue(); auto* unknownRoot = unknownVersion.getIf(); REQUIRE(unknownRoot != nullptr); unknownRoot->at("schemaVersion") = int64_t{2}; const Revision revisionBeforeRejectedMigration = restored.revision(); CHECK_EQ(static_cast(restored.loadSnapshot(unknownVersion).code()), static_cast(EditorStatus::Rejected)); CHECK_EQ(restored.revision(), revisionBeforeRejectedMigration); CHECK(restored.read(colliderSelection(restored), PropertyPath("shape.kind")).value == EditorValue("triangle-mesh")); } TEST_CASE("editor.physics.joint_exposes_body_anchor_limit_motor_and_break_authoring") { PhysicsJointTarget target("suspension"); const SelectionSnapshot selection = jointSelection(target); CHECK_EQ(target.schema(selection).typeId, std::string("physics.joint3d")); CHECK(target.schema(selection).find(PropertyPath("body.a")) != nullptr); CHECK(target.schema(selection).find(PropertyPath("joint.axis")) != nullptr); CHECK(target.schema(selection).find(PropertyPath("motor.force")) != nullptr); CHECK(target.schema(selection).find(PropertyPath("break.torque")) != nullptr); CHECK_EQ(target.validate().size(), static_cast(1)); for (const auto& [path, value] : { std::pair{PropertyPath("body.a"), EditorValue("vehicle")}, {PropertyPath("body.b"), EditorValue("wheel")}, {PropertyPath("joint.kind"), EditorValue("wheel")}, {PropertyPath("limit.enabled"), EditorValue(true)}, {PropertyPath("limit.minimum"), EditorValue(2.0)}, {PropertyPath("limit.maximum"), EditorValue(1.0)}}) { auto operation = target.makeSet(selection, path, value, PropertySetMode::Absolute); REQUIRE(operation.ok()); REQUIRE(target.applyDomainOperation(operation.value()).ok()); } CHECK_EQ(target.validate().size(), static_cast(1)); auto fixLimit = target.makeSet(selection, PropertyPath("limit.maximum"), 3.0, PropertySetMode::Absolute); REQUIRE(fixLimit.ok()); REQUIRE(target.applyDomainOperation(fixLimit.value()).ok()); CHECK(target.validate().empty()); CHECK_EQ(static_cast(target.snapshotValue().type()), static_cast(EditorValue::Type::Object)); }