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

159 lines
8.5 KiB
C++

#include "graphics/material/editor/EditorMaterialOffscreen.h"
#include "particles/editor/EditorParticleOffscreen.h"
#include "ui/editor/EditorUiOffscreen.h"
#include "fluids/editor/EditorSurfaceFluidPreview.h"
#include "graphics/Canvas.h"
#include "graphics/ICanvasFactory.h"
#include "graphics/ICanvasTarget.h"
#include "graphics/ISolidRectRenderer.h"
#include "image/ImageData.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <memory>
using namespace eve::editor;
using namespace eve::editing;
namespace {
class Canvas final : public eve::graphics::Canvas {
public:
Canvas(int width, int height) : width_(width), height_(height) {}
int getWidth() const override { return width_; } int getHeight() const override { return height_; }
eve::graphics::Texture* getTexture() override { return nullptr; }
void clear(std::optional<eve::graphics::Color>, std::optional<int>, std::optional<double>) override {}
eve::graphics::Color getPixel(int, int) override { return {}; }
eve::image::ImageData* newImageData() override { return new eve::image::ImageData(width_, height_); }
void draw(eve::graphics::Graphics*, const glm::mat4&) const override {}
void draw(eve::graphics::Canvas*, const glm::mat4&) const override {}
private: int width_, height_;
};
class Backend final : public eve::graphics::ICanvasFactory,
public eve::graphics::ICanvasTarget,
public eve::graphics::ISolidRectRenderer {
public:
eve::graphics::Canvas* newCanvas(int width, int height) override { canvas=std::make_unique<Canvas>(width,height);return canvas.get(); }
void setCanvas(eve::graphics::Canvas* value) override { current=value; }
eve::graphics::Canvas* getCanvas() const override { return current; }
void drawSolidRectRGBA(float,float,float,float,float,float,float,float) override { ++rectangles; }
std::unique_ptr<Canvas> canvas; eve::graphics::Canvas* current=nullptr; int rectangles=0;
};
GraphDocumentData particleGraph() {
eve::particles_editing::ParticleGraphDomain domain;
GraphDocument document;
for (const char* type : {"emission", "motion", "collision", "renderer", "output"}) {
auto node = domain.makeNode(GraphNodeId(type), type); REQUIRE(node.ok());
CHECK(document.createNode(node.value()).ok());
}
int sequence = 0;
for (const auto& [from, to] : std::vector<std::pair<const char*, const char*>>{
{"emission.out","motion.in"},{"motion.out","collision.in"},
{"collision.out","renderer.in"},{"renderer.out","output.in"}}) {
const auto* output = document.findPin(GraphPinId(from)); const auto* input = document.findPin(GraphPinId(to));
REQUIRE(output); REQUIRE(input);
CHECK(document.connect({StableId("edge-" + std::to_string(sequence++)), output->id, input->id},
domain.canConnect(*output, *input)).ok());
}
return document.snapshot(domain.domain());
}
}
TEST_CASE("editor.material.offscreen_adapter_produces_publishable_artifact") {
Backend backend; auto* token=reinterpret_cast<eve::graphics::Graphics*>(&backend);
GraphicsOffscreenPreviewService offscreen(token,&backend,&backend);
int draws=0;
OffscreenMaterialPreviewRenderer renderer(&offscreen,
[&](const MaterialPreviewRenderRequest& request,auto*,auto*) { CHECK(request.material.getIf<EditorValue::Object>() != nullptr);++draws;return eve::editing::applied<void>(); });
MaterialDocumentTarget material("material"); MaterialPreviewService service;
auto task=service.render(DocumentId("doc"),material,{},renderer); REQUIRE(task.ok());
CHECK_EQ(draws,1); CHECK(service.publish(DocumentId("doc"),material.revision(),task.value()).ok());
CHECK(service.publishedArtifact(DocumentId("doc")).starts_with("preview://"));
}
TEST_CASE("editor.ui.offscreen_renderer_rasterizes_visible_widget_boxes") {
Backend backend; auto* token=reinterpret_cast<eve::graphics::Graphics*>(&backend);
GraphicsOffscreenPreviewService offscreen(token,&backend,&backend);
UiDocumentTarget document("hud"); UiLayoutValue layout;layout.width=80;layout.height=40;
auto create=document.makeCreate({ObjectId("panel"),{},"panel","Panel",layout});REQUIRE(create.ok());
CHECK(document.applyDomainOperation(create.value()).ok());
UiOffscreenPreviewRenderer renderer(&offscreen,&backend);
auto artifact=renderer.render(document,320,200);REQUIRE(artifact.ok());
CHECK_EQ(backend.rectangles,1);CHECK_EQ(artifact.value().sourceRevision,document.revision());
CHECK(backend.current==nullptr);
}
TEST_CASE("editor.particles.offscreen_preview_compiles_and_budget_checks_before_draw") {
Backend backend; auto* token=reinterpret_cast<eve::graphics::Graphics*>(&backend);
GraphicsOffscreenPreviewService offscreen(token,&backend,&backend); int draws=0;
ParticleOffscreenPreviewService renderer(&offscreen,
[&](const eve::particles_editing::ParticleGraphCompileResult& compiled,
const eve::particles_editing::ParticleGraphPreviewResult& estimate, auto*, auto*) {
CHECK_EQ(compiled.documentRevision,estimate.documentRevision);++draws;return eve::editing::applied<void>(); });
ParticleOffscreenPreviewRequest request;request.previewId=StableId("particles");request.graph=particleGraph();
auto artifact=renderer.render(request);REQUIRE(artifact.ok());CHECK_EQ(draws,1);
request.seconds=-1.0;
CHECK_EQ(static_cast<int>(renderer.render(request).code()),static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(draws,1);
}
namespace {
class ParticlePresenter final : public IParticleOffscreenPresenter {
public:
int draws = 0;
EditorResult<void> draw(const ParticleOffscreenPreviewRequest& request,
const eve::particles_editing::ParticleGraphCompileResult& compiled,
const eve::particles_editing::ParticleGraphPreviewResult& estimate,
eve::graphics::Graphics*, eve::graphics::Canvas*) override {
CHECK_EQ(request.graph.revision, compiled.documentRevision);
CHECK_EQ(compiled.documentRevision, estimate.documentRevision);
++draws; return eve::editing::applied<void>();
}
};
}
TEST_CASE("editor.particles.offscreen_preview_dispatches_isolated_presenter_contract") {
Backend backend; auto* token=reinterpret_cast<eve::graphics::Graphics*>(&backend);
GraphicsOffscreenPreviewService offscreen(token,&backend,&backend);
ParticlePresenter presenter; ParticleOffscreenPreviewService renderer(&offscreen,&presenter);
ParticleOffscreenPreviewRequest request; request.previewId=StableId("isolated-particles");
request.graph=particleGraph();
auto artifact=renderer.render(request); REQUIRE(artifact.ok());
CHECK_EQ(presenter.draws,1);
ParticleEmitterOffscreenPresenter runtime;
eve::particles_editing::ParticleGraphDomain domain; const auto compiled=domain.compile(request.graph);
const auto estimate=domain.preview(request.graph,request.seconds,request.fixedStep,request.particleBudget);
request.graph.revision++;
CHECK_EQ(static_cast<int>(runtime.draw(request,compiled,estimate,nullptr,nullptr).code()),
static_cast<int>(EditorStatus::Conflict));
}
namespace {
class SurfaceFluidRenderer final : public ISurfaceFluidPreviewRenderer {
public:
int draws = 0;
EditorResult<void> draw(const SurfaceFluidPreviewSnapshot& snapshot) override {
CHECK_EQ(static_cast<int>(snapshot.status), static_cast<int>(EditorStatus::Applied));
++draws; return eve::editing::applied<void>();
}
};
}
TEST_CASE("editor.surface_fluid.offscreen_preview_replays_before_renderer_dispatch") {
Backend backend; auto* token=reinterpret_cast<eve::graphics::Graphics*>(&backend);
GraphicsOffscreenPreviewService offscreen(token,&backend,&backend);
SurfaceFluidTarget target("waterfall"); SurfaceFluidRenderer presenter;
SurfaceFluidOffscreenPreviewService renderer(&offscreen,&presenter);
SurfaceFluidPreviewRequest request; request.previewId=StableId("surface-fluid");
request.documentRevision=target.revision(); request.seconds=0.1;
request.positions={{{0.0,0.0,0.0}},{{1.0,0.0,0.0}},{{0.0,0.0,1.0}}};
request.indices={0,1,2}; request.seeds.push_back({});
auto artifact=renderer.render(target,request); REQUIRE(artifact.ok());
CHECK_EQ(presenter.draws,1); CHECK_EQ(artifact.value().sourceRevision,target.revision());
request.documentRevision++;
CHECK_EQ(static_cast<int>(renderer.render(target,request).code()),
static_cast<int>(EditorStatus::Conflict));
CHECK_EQ(presenter.draws,1);
}