#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "editor/Brush.h" #include "editor/Editor.h" #include "editor/EditorDock.h" #include "editor/EditorHistory.h" #include "editor/EditorInspector.h" #include "editor/EditorSession.h" #include "editor/EditorToolbar.h" #include "editor/GizmoManager.h" #include "editor/TileBuffer.h" #include "editor/TransformGizmo.h" #include "common/Exception.h" #include #include #include using namespace eve::editor; namespace { class TestEditorTool final : public IEditorTool { public: explicit TestEditorTool(std::string id) { desc.id = std::move(id); } const ToolDescriptor &descriptor() const override { return desc; } void activate(EditorContext &) override { ++activations; } void deactivate(EditorContext &) override { ++deactivations; } void cancel(EditorContext &) override { ++cancellations; } ToolResponse pointerEvent(EditorContext &, const EditorPointerEvent &event) override { ++pointerEvents; if (event.phase == EditorPointerEvent::Phase::Down) return ToolResponse::capture(); if (event.phase == EditorPointerEvent::Phase::Up) return ToolResponse::release(); return ToolResponse::consumed(); } ToolResponse keyEvent(EditorContext &, const EditorKeyEvent &) override { ++keyEvents; return ToolResponse::consumed(); } void update(EditorContext &, float) override { ++updates; } ToolDescriptor desc; int activations = 0; int deactivations = 0; int cancellations = 0; int pointerEvents = 0; int keyEvents = 0; int updates = 0; }; } // namespace TEST_CASE("editor.session.routes_tool_lifecycle_and_capture") { EditorSession session; TestEditorTool first("first"); TestEditorTool second("second"); CHECK(session.addTool(&first)); CHECK(session.addTool(&second)); CHECK(!session.addTool(&first)); CHECK_EQ(session.getToolCount(), 2); CHECK(session.activateTool("first")); CHECK_EQ(first.activations, 1); EditorPointerEvent down; down.phase = EditorPointerEvent::Phase::Down; down.pointerId = 7; CHECK(session.dispatchPointer(down).handled); CHECK(session.hasPointerCapture()); CHECK_EQ(session.capturedPointerId(), 7); EditorPointerEvent other = down; other.phase = EditorPointerEvent::Phase::Move; other.pointerId = 8; CHECK(!session.dispatchPointer(other).handled); CHECK_EQ(first.pointerEvents, 1); EditorPointerEvent up = down; up.phase = EditorPointerEvent::Phase::Up; CHECK(session.dispatchPointer(up).releasePointer); CHECK(!session.hasPointerCapture()); EditorKeyEvent key; key.key = "W"; key.pressed = true; CHECK(session.dispatchKey(key).handled); session.update(0.016f); CHECK_EQ(first.keyEvents, 1); CHECK_EQ(first.updates, 1); CHECK(session.activateTool("second")); CHECK_EQ(first.cancellations, 1); CHECK_EQ(first.deactivations, 1); CHECK_EQ(second.activations, 1); CHECK(session.removeTool("second")); CHECK_EQ(second.deactivations, 1); CHECK_EQ(session.activeToolId(), std::string("")); } TEST_CASE("editor.module.name") { auto *mod = Editor::create(); CHECK_EQ(mod->getName(), std::string("Editor")); CHECK_EQ(Editor::create(), mod); } TEST_CASE("editor.gizmo.translate.pick_and_drag") { std::unique_ptr g(new TransformGizmo()); g->setMode("translate"); g->setSpace("world"); g->setPosition(0.f, 0.f, 0.f); g->setSize(1.f); // Ray from +Z looking at origin, aimed near +X axis tip std::string axis = g->pick(2.f, 0.f, 5.f, -0.3f, 0.f, -1.f); // May hit x or miss depending on exact geometry; force beginDrag on x CHECK(g->beginDrag("x", 0.f, 2.f, 5.f, 0.f, 0.f, -1.f)); CHECK(g->isDragging()); CHECK_EQ(g->getActiveAxis(), std::string("x")); // Drag along +X by moving hit point CHECK(g->updateDrag(1.5f, 2.f, 5.f, 0.f, 0.f, -1.f)); CHECK(g->getPositionX() > 0.f); float y = g->getPositionY(); float z = g->getPositionZ(); CHECK(std::fabs(y) < 1e-3f); CHECK(std::fabs(z) < 1e-3f); g->endDrag(); CHECK(!g->isDragging()); CHECK_EQ(g->getPartCount() > 0, true); CHECK_EQ(g->getPartKind(0), std::string("axis")); (void)axis; } TEST_CASE("editor.gizmo.snap_translate") { std::unique_ptr g(new TransformGizmo()); g->setMode("translate"); g->setSnapTranslate(1.f, 1.f, 1.f); g->setPosition(0.f, 0.f, 0.f); CHECK(g->beginDrag("x", 0.f, 2.f, 5.f, 0.f, 0.f, -1.f)); g->updateDrag(0.4f, 2.f, 5.f, 0.f, 0.f, -1.f); // With snap 1, small moves round toward 0 or 1 float x = g->getPositionX(); CHECK(std::fabs(x - std::round(x)) < 1e-4f); g->endDrag(); } TEST_CASE("editor.gizmo.rotate_and_scale_modes") { std::unique_ptr g(new TransformGizmo()); g->setMode("rotate"); CHECK_EQ(g->getPartCount(), 3); CHECK_EQ(g->getPartKind(0), std::string("ring")); g->setMode("scale"); CHECK(g->getPartCount() >= 3); CHECK(g->beginDrag("xyz", 0.f, 0.f, 5.f, 0.f, 0.f, -1.f)); float s0 = g->getScaleX(); g->updateDrag(0.f, 1.f, 5.f, 0.f, 0.f, -1.f); // uniform scale may change CHECK(g->getScaleX() > 0.f); CHECK_EQ(g->getScaleX(), g->getScaleY()); (void)s0; g->endDrag(); g->setMode("bound"); CHECK(g->getPartCount() >= 6); } TEST_CASE("editor.gizmo.invalid_mode") { std::unique_ptr g(new TransformGizmo()); bool threw = false; try { g->setMode("skew"); } catch (const eve::Exception &) { threw = true; } CHECK(threw); } TEST_CASE("editor.gizmo_manager.multi_mode") { std::unique_ptr mgr(new GizmoManager()); CHECK(!mgr->isAttached()); CHECK(mgr->pick(0, 0, 5, 0, 0, -1).empty()); mgr->attach(); mgr->setPositionEnabled(true); mgr->setRotationEnabled(true); TransformGizmo *g = mgr->getGizmo(); g->setPosition(0.f, 0.f, 0.f); g->setSize(1.f); // Pick should succeed on some handle when looking at origin std::string axis = mgr->pick(0.f, 0.f, 5.f, 0.f, 0.f, -1.f); // center or axis may be hit if (!axis.empty()) { CHECK(mgr->beginDrag(axis, 0.f, 0.f, 5.f, 0.f, 0.f, -1.f)); mgr->endDrag(); } mgr->detach(); CHECK(!mgr->isAttached()); } TEST_CASE("editor.brush.paint_fill_line_rect") { std::unique_ptr ed(Editor::create()); // Module singleton — don't delete; use factories only std::unique_ptr buf(new TileBuffer(8, 8)); std::unique_ptr brush(new Brush()); brush->setTool("paint"); brush->setTile(3); brush->setSize(1); CHECK_EQ(brush->paintAt(buf.get(), 2, 2), 1); CHECK_EQ(buf->getGid(2, 2), 3); brush->setSize(3); brush->setShape("square"); int n = brush->paintAt(buf.get(), 4, 4); CHECK(n >= 1); CHECK_EQ(buf->getGid(4, 4), 3); buf->fill(1); buf->setGid(0, 0, 0); brush->setTile(7); CHECK(brush->floodFill(buf.get(), 1, 1) > 1); CHECK_EQ(buf->getGid(3, 3), 7); CHECK_EQ(buf->getGid(0, 0), 0); // separated buf->clear(); brush->setTile(2); brush->setSize(1); CHECK(brush->paintLine(buf.get(), 0, 0, 3, 0) >= 4); CHECK_EQ(buf->getGid(2, 0), 2); buf->clear(); CHECK(brush->paintRect(buf.get(), 1, 1, 3, 3, true) >= 9); CHECK_EQ(buf->getGid(2, 2), 2); brush->previewAt(buf.get(), 5, 5); CHECK(brush->getPreviewCount() >= 1); } TEST_CASE("editor.brush.stamp_and_erase") { std::unique_ptr buf(new TileBuffer(5, 5)); std::unique_ptr brush(new Brush()); brush->setStampSize(2, 2); brush->setStampTile(0, 0, 9); brush->setStampTile(1, 0, 8); brush->setStampTile(0, 1, 7); brush->setStampTile(1, 1, 6); brush->setTool("stamp"); CHECK(brush->paintAt(buf.get(), 2, 2) >= 1); CHECK(brush->getChangeCount() >= 1); brush->setTool("erase"); brush->setEraseTile(0); brush->setSize(1); brush->eraseAt(buf.get(), 2, 2); } TEST_CASE("editor.toolbar_inspector_dock") { std::unique_ptr tb(new EditorToolbar()); tb->addTool("move", "Move"); tb->addTool("paint", "Paint"); tb->setShortcut("move", "W"); CHECK_EQ(tb->getActive(), std::string("move")); CHECK(tb->matchShortcut("W")); CHECK(tb->setActive("paint")); CHECK_EQ(tb->getActive(), std::string("paint")); CHECK_EQ(tb->getToolCount(), 2); std::unique_ptr insp(new EditorInspector()); insp->addFloat("size", "Size", 1.f, 0.f, 10.f, 0.1f); insp->addFloat3("pos", "Position", 1.f, 2.f, 3.f); insp->addBool("snap", "Snap", false); insp->addChoice("mode", "Mode", "translate,rotate,scale", "translate"); insp->setFloat("size", 2.5f); CHECK(insp->isDirty("size")); CHECK_EQ(insp->pollChangedId(), std::string("size")); CHECK_EQ(insp->getFloat3Y("pos"), 2.f); insp->setChoice("mode", "rotate"); CHECK_EQ(insp->getChoice("mode"), std::string("rotate")); std::unique_ptr dock(new EditorDock()); dock->setRegionSize("left", 100.f); dock->setRegionSize("right", 120.f); dock->setRegionSize("top", 30.f); dock->setRegionSize("bottom", 20.f); dock->layout(800.f, 600.f); CHECK_EQ(dock->getRegionW("center"), 800.f - 100.f - 120.f); CHECK_EQ(dock->getRegionH("center"), 600.f - 30.f - 20.f); CHECK_EQ(dock->getRegionX("right"), 800.f - 120.f); } TEST_CASE("editor.history.tiles_undo_redo") { std::unique_ptr buf(new TileBuffer(4, 4)); std::unique_ptr brush(new Brush()); std::unique_ptr hist(new EditorHistory()); brush->setTile(5); brush->paintAt(buf.get(), 1, 1); hist->beginGroup("paint"); for (int i = 0; i < brush->getChangeCount(); ++i) { hist->recordTile(brush->getChangeX(i), brush->getChangeY(i), brush->getChangeOldGid(i), brush->getChangeNewGid(i)); } hist->endGroup(); CHECK_EQ(buf->getGid(1, 1), 5); CHECK(hist->canUndo()); CHECK(hist->undo()); CHECK(hist->applyLastToBuffer(buf.get())); CHECK_EQ(buf->getGid(1, 1), 0); CHECK(hist->canRedo()); CHECK(hist->redo()); CHECK(hist->applyLastToBuffer(buf.get())); CHECK_EQ(buf->getGid(1, 1), 5); hist->push("rename", "npc_01"); CHECK(hist->undo()); CHECK_EQ(hist->getLastActionKind(), std::string("opaque")); CHECK_EQ(hist->getLastPayload(), std::string("npc_01")); } TEST_CASE("editor.factories") { Editor *ed = Editor::create(); std::unique_ptr g(ed->newGizmo()); std::unique_ptr m(ed->newGizmoManager()); std::unique_ptr b(ed->newTileBuffer(2, 2)); std::unique_ptr br(ed->newBrush()); std::unique_ptr t(ed->newToolbar()); std::unique_ptr i(ed->newInspector()); std::unique_ptr d(ed->newDock()); std::unique_ptr h(ed->newHistory()); // zeroerr CHECK copies the expression for printing — use raw pointers, not unique_ptr. CHECK(g.get() != nullptr); CHECK(m.get() != nullptr); CHECK_EQ(b->getWidth(), 2); CHECK(br.get() != nullptr); CHECK(t.get() != nullptr); CHECK(i.get() != nullptr); CHECK(d.get() != nullptr); CHECK(h.get() != nullptr); }