#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 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, std::optional, std::optional) 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(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; 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>{ {"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(&backend); GraphicsOffscreenPreviewService offscreen(token,&backend,&backend); int draws=0; OffscreenMaterialPreviewRenderer renderer(&offscreen, [&](const MaterialPreviewRenderRequest& request,auto*,auto*) { CHECK(request.material.getIf() != nullptr);++draws;return eve::editing::applied(); }); 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(&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(&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(); }); 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(renderer.render(request).code()),static_cast(EditorStatus::Rejected)); CHECK_EQ(draws,1); } namespace { class ParticlePresenter final : public IParticleOffscreenPresenter { public: int draws = 0; EditorResult 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(); } }; } TEST_CASE("editor.particles.offscreen_preview_dispatches_isolated_presenter_contract") { Backend backend; auto* token=reinterpret_cast(&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(runtime.draw(request,compiled,estimate,nullptr,nullptr).code()), static_cast(EditorStatus::Conflict)); } namespace { class SurfaceFluidRenderer final : public ISurfaceFluidPreviewRenderer { public: int draws = 0; EditorResult draw(const SurfaceFluidPreviewSnapshot& snapshot) override { CHECK_EQ(static_cast(snapshot.status), static_cast(EditorStatus::Applied)); ++draws; return eve::editing::applied(); } }; } TEST_CASE("editor.surface_fluid.offscreen_preview_replays_before_renderer_dispatch") { Backend backend; auto* token=reinterpret_cast(&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(renderer.render(target,request).code()), static_cast(EditorStatus::Conflict)); CHECK_EQ(presenter.draws,1); }