Files
EVEngine/test/editor_map_document.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

188 lines
8.6 KiB
C++

#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<void> publishRoad(const std::string&, const StableId& road,
Revision revision, const EditorValue& mesh) override {
published = road; publishedRevision = revision; value = mesh;
return eve::editing::applied<void>();
}
EditorResult<void> removeRoad(const std::string&, const StableId&) override {
return eve::editing::applied<void>();
}
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<int>(publisher.publish(map, StableId("main"), map.revision() - 1, sink).code()),
static_cast<int>(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<int>(imported.status), static_cast<int>(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<int>(map.makeDeleteLayer(StableId("roads")).code()),
static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(static_cast<int>(map.makeCreateLayer({StableId("duplicate-order"), "Duplicate", "tile",
true, false, 1.0, 10}).code()),
static_cast<int>(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<int>(preview.status), static_cast<int>(EditorStatus::Applied));
CHECK(preview.centerlineLength > 20.0);
const auto* mesh = preview.mesh.getIf<EditorValue::Object>();
REQUIRE(mesh);
CHECK_EQ(mesh->at("vertices").getIf<EditorValue::Array>()->size(), size_t{6});
CHECK_EQ(mesh->at("indices").getIf<EditorValue::Array>()->size(), size_t{12});
CHECK_EQ(static_cast<int>(map.previewRoad(StableId("main-road"), 1).status),
static_cast<int>(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<int>(map.makeSetRoad(road()).code()), static_cast<int>(EditorStatus::Rejected));
auto staged = map.cloneDomainState();
auto* candidate = dynamic_cast<MapDocumentTarget*>(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<EditorValue::Object>();
REQUIRE(root);
(*root)["schemaVersion"] = int64_t{9};
CHECK_EQ(static_cast<int>(restored.loadSnapshot(broken).code()),
static_cast<int>(EditorStatus::Unsupported));
CHECK_EQ(restored.revision(), before);
CHECK_EQ(restored.snapshotValue(), snapshot);
}