#include "map/editing/MapDocument.h" #include "zeroerr/assert.h" #include "zeroerr/unittest.h" using namespace eve::map_editing; using namespace eve::editing; namespace { class RoadSink final : public IMapRoadMeshSink { public: EditorResult publishRoad(const std::string&, const StableId& road, Revision revision, const EditorValue& mesh) override { published = road; publishedRevision = revision; value = mesh; return eve::editing::applied(); } EditorResult removeRoad(const std::string&, const StableId&) override { return eve::editing::applied(); } StableId published; Revision publishedRevision = 0; EditorValue value; }; } TEST_CASE("editor.map.road_mesh_publication_and_object_import_are_revision_safe") { MapDocumentTarget map("city"); auto roadLayer = map.makeCreateLayer({StableId("roads"), "Roads", "road", true, false, 1.0, 0}); REQUIRE(roadLayer.ok()); CHECK(map.applyDomainOperation(roadLayer.value()).ok()); auto objectLayer = map.makeCreateLayer({StableId("objects"), "Objects", "object", true, false, 1.0, 1}); REQUIRE(objectLayer.ok()); CHECK(map.applyDomainOperation(objectLayer.value()).ok()); MapRoadRecord road{StableId("main"), StableId("roads"), "Main", "asphalt", false, {{StableId("a"), 0.0, 0.0, 0.0, 4.0}, {StableId("b"), 10.0, 0.0, 0.0, 4.0}}}; auto setRoad = map.makeSetRoad(road); REQUIRE(setRoad.ok()); CHECK(map.applyDomainOperation(setRoad.value()).ok()); RoadSink sink; MapRoadMeshPublisher publisher; CHECK(publisher.publish(map, StableId("main"), map.revision(), sink).ok()); CHECK_EQ(sink.published.value(), "main"); CHECK_EQ(sink.publishedRevision, map.revision()); CHECK_EQ(static_cast(publisher.publish(map, StableId("main"), map.revision() - 1, sink).code()), static_cast(EditorStatus::Conflict)); MapObjectImporter importer; const auto imported = importer.plan(map, StableId("objects"), {{"tree-01", "asset://tree", 2.0, 0.0, 3.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0}}, "import/forest"); CHECK_EQ(static_cast(imported.status), static_cast(EditorStatus::Applied)); REQUIRE_EQ(imported.operations.size(), 1U); CHECK(map.applyDomainOperation(imported.operations[0]).ok()); CHECK_EQ(map.mapPlacements()[0].id.value(), "import/forest/tree-01"); } namespace { DomainOperation inverse(const DomainOperation& source) { DomainOperation result = source; result.type = source.inverseType; result.payload = source.inverse; return result; } void createLayer(MapDocumentTarget& map, MapLayerRecord layer) { auto operation = map.makeCreateLayer(layer); REQUIRE(operation.ok()); REQUIRE(map.applyDomainOperation(operation.value()).ok()); } MapRoadRecord road() { return {StableId("main-road"), StableId("roads"), "Main Road", "asset://materials/asphalt", false, {{StableId("p0"), 0.0, 0.0, 0.0, 4.0}, {StableId("p1"), 10.0, 0.0, 2.0, 5.0}, {StableId("p2"), 20.0, 0.0, 0.0, 4.0}}}; } } // namespace TEST_CASE("editor.map.layers_are_ordered_reversible_and_protect_nonempty_content") { MapDocumentTarget map("world-map"); createLayer(map, {StableId("ground"), "Ground", "tile", true, false, 1.0, 0}); createLayer(map, {StableId("roads"), "Roads", "road", true, false, 0.9, 10}); createLayer(map, {StableId("objects"), "Objects", "object", true, false, 1.0, 20}); REQUIRE_EQ(map.mapLayers().size(), size_t{3}); CHECK_EQ(map.mapLayers()[0].id, StableId("ground")); CHECK_EQ(map.mapLayers()[2].id, StableId("objects")); MapLayerRecord changed = map.mapLayers()[2]; changed.name = "Props"; changed.visible = false; auto set = map.makeSetLayer(changed); REQUIRE(set.ok()); REQUIRE(map.applyDomainOperation(set.value()).ok()); CHECK_EQ(map.mapLayers()[2].name, "Props"); REQUIRE(map.applyDomainOperation(inverse(set.value())).ok()); CHECK_EQ(map.mapLayers()[2].name, "Objects"); auto addRoad = map.makeSetRoad(road()); REQUIRE(addRoad.ok()); REQUIRE(map.applyDomainOperation(addRoad.value()).ok()); CHECK_EQ(static_cast(map.makeDeleteLayer(StableId("roads")).code()), static_cast(EditorStatus::Rejected)); CHECK_EQ(static_cast(map.makeCreateLayer({StableId("duplicate-order"), "Duplicate", "tile", true, false, 1.0, 10}).code()), static_cast(EditorStatus::Conflict)); } TEST_CASE("editor.map.road_spline_and_placements_support_create_update_undo") { MapDocumentTarget map("content-map"); createLayer(map, {StableId("roads"), "Roads", "road", true, false, 1.0, 0}); createLayer(map, {StableId("objects"), "Objects", "object", true, false, 1.0, 1}); auto addRoad = map.makeSetRoad(road()); REQUIRE(addRoad.ok()); REQUIRE(map.applyDomainOperation(addRoad.value()).ok()); CHECK_EQ(map.mapRoads()[0].points.size(), size_t{3}); const auto preview = map.previewRoad(StableId("main-road")); CHECK_EQ(static_cast(preview.status), static_cast(EditorStatus::Applied)); CHECK(preview.centerlineLength > 20.0); const auto* mesh = preview.mesh.getIf(); REQUIRE(mesh); CHECK_EQ(mesh->at("vertices").getIf()->size(), size_t{6}); CHECK_EQ(mesh->at("indices").getIf()->size(), size_t{12}); CHECK_EQ(static_cast(map.previewRoad(StableId("main-road"), 1).status), static_cast(EditorStatus::Rejected)); MapRoadRecord changed = map.mapRoads()[0]; changed.points[1].width = 8.0; changed.closed = true; auto updateRoad = map.makeSetRoad(changed); REQUIRE(updateRoad.ok()); REQUIRE(map.applyDomainOperation(updateRoad.value()).ok()); CHECK_EQ(map.mapRoads()[0].points[1].width, 8.0); REQUIRE(map.applyDomainOperation(inverse(updateRoad.value())).ok()); CHECK_EQ(map.mapRoads()[0].points[1].width, 5.0); MapPlacementRecord tree{StableId("tree-1"), StableId("objects"), "asset://models/tree.evm", 4.0, 0.0, 8.0, 0.0, 30.0, 0.0, 1.0, 1.0, 1.0}; auto place = map.makeSetPlacement(tree); REQUIRE(place.ok()); REQUIRE(map.applyDomainOperation(place.value()).ok()); CHECK_EQ(map.mapPlacements()[0].asset, "asset://models/tree.evm"); REQUIRE(map.applyDomainOperation(inverse(place.value())).ok()); CHECK(map.mapPlacements().empty()); } TEST_CASE("editor.map.locked_layers_reject_content_edits_and_batch_staging_is_atomic") { MapDocumentTarget map("locked-map"); createLayer(map, {StableId("roads"), "Roads", "road", true, true, 1.0, 0}); CHECK_EQ(static_cast(map.makeSetRoad(road()).code()), static_cast(EditorStatus::Rejected)); auto staged = map.cloneDomainState(); auto* candidate = dynamic_cast(staged.get()); REQUIRE(candidate); auto unlock = candidate->makeSetLayer({StableId("roads"), "Roads", "road", true, false, 1.0, 0}); REQUIRE(unlock.ok()); REQUIRE(candidate->applyDomainOperation(unlock.value()).ok()); auto addRoad = candidate->makeSetRoad(road()); REQUIRE(addRoad.ok()); REQUIRE(candidate->applyDomainOperation(addRoad.value()).ok()); REQUIRE(map.commitDomainState(std::move(staged)).ok()); CHECK_EQ(map.mapRoads().size(), size_t{1}); } TEST_CASE("editor.map.snapshot_round_trip_and_geometry_diagnostics") { MapDocumentTarget map("snapshot-map"); createLayer(map, {StableId("roads"), "Roads", "road", true, false, 1.0, 0}); createLayer(map, {StableId("objects"), "Objects", "object", true, false, 1.0, 1}); MapRoadRecord zero = road(); zero.materialAsset.clear(); zero.points[1].x = zero.points[0].x; zero.points[1].z = zero.points[0].z; zero.points[2].x = zero.points[0].x; zero.points[2].z = zero.points[0].z; auto add = map.makeSetRoad(zero); REQUIRE(add.ok()); REQUIRE(map.applyDomainOperation(add.value()).ok()); CHECK_EQ(map.validate().size(), size_t{2}); const EditorValue snapshot = map.snapshotValue(); MapDocumentTarget restored("snapshot-map"); REQUIRE(restored.loadSnapshot(snapshot).ok()); CHECK_EQ(restored.snapshotValue(), snapshot); const Revision before = restored.revision(); EditorValue broken = snapshot; auto* root = broken.getIf(); REQUIRE(root); (*root)["schemaVersion"] = int64_t{9}; CHECK_EQ(static_cast(restored.loadSnapshot(broken).code()), static_cast(EditorStatus::Unsupported)); CHECK_EQ(restored.revision(), before); CHECK_EQ(restored.snapshotValue(), snapshot); }