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>
142 lines
6.8 KiB
C++
142 lines
6.8 KiB
C++
#include "procgen/biome/editing/BiomeTarget.h"
|
|
#include "procgen/biome/editor/BiomeEditorModule.h"
|
|
#include "common/Capability.h"
|
|
#include "common/EditorAutomation.h"
|
|
#include "editor/Editor.h"
|
|
#include "procgen/PointSet.h"
|
|
#include "procgen/SpatialData.h"
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
#include <map>
|
|
using namespace eve::biome_editing;
|
|
using namespace eve::editing;
|
|
namespace {
|
|
SelectionSnapshot select(const BiomeDocumentTarget& target, const ObjectId& id,
|
|
const std::string& type) {
|
|
SelectionSnapshot selection;
|
|
selection.channel = "biome";
|
|
selection.items.push_back(
|
|
{SelectionDomain::Asset, TargetId(target.targetId()), StableId(id.value()), type});
|
|
return selection;
|
|
}
|
|
void apply(BiomeDocumentTarget& target, EditorResult<DomainOperation> operation) {
|
|
REQUIRE(operation.ok());
|
|
REQUIRE(target.applyDomainOperation(operation.value()).ok());
|
|
}
|
|
class SpatialResolver final : public IBiomeSpatialResolver {
|
|
public:
|
|
EditorResult<eve::procgen::SpatialData*> resolve(const std::string& asset) const override {
|
|
const auto found = assets.find(asset);
|
|
if (found == assets.end())
|
|
return eve::editing::failed<eve::procgen::SpatialData*>(
|
|
EditorStatus::NotFound, RuleId("test.biome.spatial"), "missing spatial asset");
|
|
return eve::editing::applied<eve::procgen::SpatialData*>(found->second);
|
|
}
|
|
std::map<std::string, eve::procgen::SpatialData*> assets;
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("editor.biome.properties_are_reversible_and_snapshot_load_is_atomic") {
|
|
BiomeDocumentTarget target("world.biome");
|
|
BiomeLayerValue layer{ObjectId("forest"), "forest", "areas/forest.spatial", 1, 1.f, {}};
|
|
apply(target, target.makeCreateLayer(layer));
|
|
apply(target, target.makeCreateAsset(
|
|
layer.id, {ObjectId("oak"), "models/oak.prefab", 2.f, .8f, 1.2f, true}));
|
|
const auto selection = select(target, ObjectId("oak"), "biome.asset");
|
|
const auto scale = target.makeSet(selection, PropertyPath("asset.scale"),
|
|
EditorValue::Array{.5, 1.5}, PropertySetMode::Absolute);
|
|
REQUIRE(scale.ok());
|
|
REQUIRE(target.applyDomainOperation(scale.value()).ok());
|
|
CHECK_EQ(target.layers()[0].assets[0].minScale, .5f);
|
|
DomainOperation undo = scale.value();
|
|
undo.payload = scale.value().inverse;
|
|
REQUIRE(target.applyDomainOperation(undo).ok());
|
|
CHECK_EQ(target.layers()[0].assets[0].minScale, .8f);
|
|
|
|
const EditorValue before = target.snapshotValue();
|
|
EditorValue invalid = before;
|
|
auto* root = invalid.getIf<EditorValue::Object>();
|
|
auto* content = (*root)["content"].getIf<EditorValue::Object>();
|
|
auto* layers = (*content)["layers"].getIf<EditorValue::Array>();
|
|
auto* first = (*layers)[0].getIf<EditorValue::Object>();
|
|
(*first)["density"] = 2.0;
|
|
CHECK_EQ(static_cast<int>(target.loadSnapshot(invalid).code()),
|
|
static_cast<int>(EditorStatus::Rejected));
|
|
CHECK_EQ(target.snapshotValue(), before);
|
|
}
|
|
|
|
TEST_CASE("editor.biome.runtime_publishes_candidates_and_previews_deterministically") {
|
|
BiomeDocumentTarget target("world.biome");
|
|
apply(target, target.makeCreateLayer(
|
|
{ObjectId("forest"), "forest", "areas/forest.spatial", 1, 1.f, {}}));
|
|
apply(target, target.makeCreateAsset(
|
|
ObjectId("forest"),
|
|
{ObjectId("oak"), "models/oak.prefab", 1.f, 1.f, 1.f, false}));
|
|
eve::procgen::SpatialData domain =
|
|
eve::procgen::SpatialData::box(0.f, 0.f, 0.f, 4.f, 0.f, 4.f);
|
|
SpatialResolver resolver;
|
|
resolver.assets["areas/forest.spatial"] = &domain;
|
|
BiomeDocumentRuntime runtime;
|
|
REQUIRE(runtime.publish(target, resolver).ok());
|
|
const Revision published = runtime.revision();
|
|
auto first = runtime.preview(&domain, 2.f, 42, 0.f, published);
|
|
auto second = runtime.preview(&domain, 2.f, 42, 0.f, published);
|
|
REQUIRE(first.ok());
|
|
REQUIRE(second.ok());
|
|
REQUIRE(bool(first.value()));
|
|
REQUIRE(bool(second.value()));
|
|
CHECK_EQ(first.value()->getCount(), second.value()->getCount());
|
|
for (int i = 0; i < first.value()->getCount(); ++i) {
|
|
CHECK_EQ(first.value()->getStringAttribute(i, "asset", ""),
|
|
second.value()->getStringAttribute(i, "asset", ""));
|
|
CHECK_EQ(first.value()->getPointSeed(i), second.value()->getPointSeed(i));
|
|
}
|
|
CHECK_EQ(static_cast<int>(runtime.preview(&domain, 2.f, 42, 0.f, published + 1).code()),
|
|
static_cast<int>(EditorStatus::Conflict));
|
|
SpatialResolver missing;
|
|
CHECK_EQ(static_cast<int>(runtime.publish(target, missing).code()),
|
|
static_cast<int>(EditorStatus::NotFound));
|
|
CHECK_EQ(runtime.revision(), published);
|
|
}
|
|
|
|
TEST_CASE("editor.biome.adapter_registers_commands_and_owns_automation_targets") {
|
|
using eve::editor::Editor;
|
|
Editor editor;
|
|
eve::biome_editor::BiomeEditorModule adapter;
|
|
auto* automation = eve::cap::query<eve::IEditorAutomation>();
|
|
REQUIRE(automation != nullptr);
|
|
|
|
const std::string commands = automation->invoke("commands", "{}");
|
|
CHECK(commands.find("biome.layer.create.v1") != std::string::npos);
|
|
CHECK(commands.find("biome.asset.create.v1") != std::string::npos);
|
|
CHECK(commands.find("biome.property.set.v1") != std::string::npos);
|
|
|
|
const std::string created = automation->invoke(
|
|
"target-create",
|
|
R"({"target":"agent.biome","type":"biome","layer":"forest","spatial":"areas/forest.spatial"})");
|
|
REQUIRE(created.find("\"status\":\"applied\"") != std::string::npos);
|
|
|
|
const std::string asset = automation->invoke(
|
|
"execute",
|
|
R"({"target":"agent.biome","command":"biome.asset.create.v1","payload":{"layer":"forest","id":"oak","asset":"models/oak.prefab","weight":2,"minScale":0.8,"maxScale":1.2}})");
|
|
REQUIRE(asset.find("\"status\":\"applied\"") != std::string::npos);
|
|
|
|
const std::string density = automation->invoke(
|
|
"execute",
|
|
R"({"target":"agent.biome","command":"biome.property.set.v1","payload":{"item":"forest","type":"biome.layer","path":"layer.density","value":0.5}})");
|
|
REQUIRE(density.find("\"status\":\"applied\"") != std::string::npos);
|
|
|
|
const std::string inspected = automation->invoke("inspect", R"({"target":"agent.biome"})");
|
|
CHECK(inspected.find("forest") != std::string::npos);
|
|
CHECK(inspected.find("oak") != std::string::npos);
|
|
CHECK(inspected.find("0.5") != std::string::npos);
|
|
|
|
const std::string undone = automation->invoke("undo", R"({"target":"agent.biome"})");
|
|
CHECK(undone.find("\"status\":\"applied\"") != std::string::npos);
|
|
const std::string afterUndo = automation->invoke("inspect", R"({"target":"agent.biome"})");
|
|
CHECK(afterUndo.find("0.5") == std::string::npos);
|
|
|
|
const std::string closed = automation->invoke("target-close", R"({"target":"agent.biome"})");
|
|
CHECK(closed.find("\"status\":\"applied\"") != std::string::npos);
|
|
}
|