Files
EVEngine/test/editor_decal_target.cpp
T
西风逍遥游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

86 lines
5.6 KiB
C++

#include "decal/editing/DecalTarget.h"
#include "decal/DecalManager.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <map>
using namespace eve::decal_editing;
using namespace eve::editing;
namespace {
SelectionSnapshot selection(const char* target) {
SelectionSnapshot value;
value.items.push_back({SelectionDomain::Asset,TargetId(target),StableId("decal"),"decal.instance"});
return value;
}
class Assets final : public IDecalRuntimeAssetResolver {
public:
std::map<std::string,eve::graphics::Texture*> values;
EditorResult<eve::graphics::Texture*> texture(const std::string& asset) const override {
auto found=values.find(asset);if(found==values.end())return eve::editing::failed<eve::graphics::Texture*>(
EditorStatus::NotFound,RuleId("test.decal.texture"),"missing texture");
return eve::editing::applied<eve::graphics::Texture*>(found->second);
}
};
EditorResult<void> set(DecalDocumentTarget&target,const char*path,EditorValue value){auto op=target.makeSet(selection(target.targetId().value().c_str()),PropertyPath(path),value,PropertySetMode::Absolute);if(!op.ok())return EditorResult<void>::failure(op.status());return target.applyDomainOperation(op.value());}
}
TEST_CASE("editor.decal.properties_are_reversible_persistent_and_gizmo_ready") {
DecalDocumentTarget target("scorch");
auto operation=target.makeSet(selection("scorch"),PropertyPath("projection.size"),2.5,PropertySetMode::Absolute);
REQUIRE(operation.ok());REQUIRE(target.applyDomainOperation(operation.value()).ok());
CHECK_EQ(*target.value("projection.size")->getIf<double>(),2.5);
DomainOperation undo=operation.value();undo.payload=operation.value().inverse;
REQUIRE(target.applyDomainOperation(undo).ok());CHECK_EQ(*target.value("projection.size")->getIf<double>(),0.5);
REQUIRE(set(target,"texture.albedo","textures/scorch.png").ok());
auto gizmo=DecalGizmoPreviewService().build(target);CHECK_EQ(static_cast<int>(gizmo.status),static_cast<int>(EditorStatus::Applied));
REQUIRE_EQ(gizmo.primitives.size(),static_cast<std::size_t>(2));CHECK_EQ(gizmo.primitives[0].kind,std::string("oriented-wire-box"));
DecalDocumentTarget restored("copy");REQUIRE(restored.loadSnapshot(target.snapshotValue()).ok());
CHECK_EQ(*restored.value("texture.albedo")->getIf<std::string>(),std::string("textures/scorch.png"));
}
TEST_CASE("editor.decal.snapshot_is_atomic_and_rejects_projection_rules") {
DecalDocumentTarget target("scorch");const auto before=target.snapshotValue();
EditorValue invalid=before;auto*root=invalid.getIf<EditorValue::Object>();auto*values=(*root)["values"].getIf<EditorValue::Object>();
(*values)["transform.normal"]=EditorValue::Array{0.0,0.0,0.0};
CHECK_EQ(static_cast<int>(target.loadSnapshot(invalid).code()),static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(target.snapshotValue(),before);
CHECK_EQ(static_cast<int>(target.makeSet(selection("scorch"),PropertyPath("texture.uvRect"),
EditorValue::Array{0.8,0.0,0.4,1.0},PropertySetMode::Absolute).code()),static_cast<int>(EditorStatus::Rejected));
}
TEST_CASE("editor.decal.runtime_replacement_resolves_assets_before_atomic_generation_swap") {
auto&manager=eve::decal::DecalManager::inst();manager.clearAll();manager.setLimit("scorch",1);
DecalDocumentTarget target("scorch");REQUIRE(set(target,"decal.kind","scorch").ok());
REQUIRE(set(target,"texture.albedo","textures/a.png").ok());
Assets assets;auto*texture=reinterpret_cast<eve::graphics::Texture*>(0x1);assets.values["textures/a.png"]=texture;
DecalRuntimeBinding binding(&manager,&assets);REQUIRE(binding.publish(target).ok());
const int first=binding.runtimeId();REQUIRE(first>0);CHECK_EQ(manager.count(),1);
REQUIRE(set(target,"texture.albedo","textures/missing.png").ok());
CHECK_EQ(static_cast<int>(binding.publish(target).code()),static_cast<int>(EditorStatus::NotFound));
CHECK_EQ(binding.runtimeId(),first);CHECK_EQ(manager.count(),1);CHECK_EQ(manager.instances()[0].id,first);
REQUIRE(set(target,"texture.albedo","textures/a.png").ok());REQUIRE(set(target,"projection.depth",0.7).ok());
REQUIRE(binding.publish(target).ok());CHECK(binding.runtimeId()!=first);CHECK_EQ(manager.count(),1);
CHECK_EQ(manager.instances()[0].depth,0.7f);REQUIRE(binding.clear().ok());CHECK_EQ(manager.count(),0);
}
TEST_CASE("editor.decal.publishing_target_preserves_author_and_runtime_on_rejection") {
auto&manager=eve::decal::DecalManager::inst();manager.clearAll();
Assets assets;assets.values["textures/a.png"]=reinterpret_cast<eve::graphics::Texture*>(0x1);
DecalRuntimeBinding runtime(&manager,&assets);DecalPublishingTarget target("live",&runtime);
auto first=target.authoringTarget().makeSet(selection("live"),PropertyPath("texture.albedo"),
"textures/a.png",PropertySetMode::Absolute);
REQUIRE(first.ok());REQUIRE(target.applyDomainOperation(first.value()).ok());
const int generation=runtime.runtimeId();REQUIRE(generation>0);
auto missing=target.authoringTarget().makeSet(selection("live"),PropertyPath("texture.albedo"),
"textures/missing.png",PropertySetMode::Absolute);
REQUIRE(missing.ok());CHECK_EQ(static_cast<int>(target.applyDomainOperation(missing.value()).code()),
static_cast<int>(EditorStatus::NotFound));
CHECK_EQ(*target.authoringTarget().value("texture.albedo")->getIf<std::string>(),
std::string("textures/a.png"));
CHECK_EQ(runtime.runtimeId(),generation);CHECK_EQ(manager.instances()[0].id,generation);
REQUIRE(runtime.clear().ok());
}