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>
159 lines
8.5 KiB
C++
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);
|
|
}
|