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

228 lines
8.7 KiB
C++

#include "building/editor/EditorBuildingTarget.h"
#include "building/BuildingDef.h"
#include "building/PlacementWorld.h"
#include "editing/EditingAuthority.h"
#include "editor/EditorSession.h"
#include "zeroerr/unittest.h"
#include <cmath>
#include <span>
namespace {
using eve::editor::BuildingEdgeCurveTool;
using eve::editor::BuildingEdgeCurveToolSelection;
using eve::editor::BuildingPlacementTarget;
using eve::editor::BuildingViewportRay;
using eve::editor::EditorPointerEvent;
using eve::editor::IBuildingViewportAdapter;
using eve::editor::IEditorOverlay;
using eve::editor::OverlayPoint;
using eve::editor::OverlayStyle;
void registerToolFence() {
eve::building::BuildingDefinition fence;
fence.id = "editor-tool-fence";
fence.displayName = "Tool Fence";
fence.placementKind = "edge";
fence.channel = "boundary";
eve::building::BuildingRegistry::registerBuilding(fence);
}
class TestBuildingViewport final : public IBuildingViewportAdapter {
public:
eve::editing::Result<BuildingViewportRay> pointerRay(
const EditorPointerEvent& event) const override {
BuildingViewportRay ray;
ray.origin = {event.x, 30.0, event.y};
ray.direction = {0.0, -1.0, 0.0};
return eve::editing::applied<BuildingViewportRay>(ray);
}
eve::editing::Result<OverlayPoint> projectWorld(
const std::array<double, 3>& world) const override {
return eve::editing::applied<OverlayPoint>(
{static_cast<float>(world[0]), static_cast<float>(world[2]),
static_cast<float>(world[1])});
}
};
class CountingAuthority final : public eve::editing::IEditAuthority {
public:
explicit CountingAuthority(eve::editing::IDomainOperationTarget* target)
: local_(target) {}
eve::editing::Result<eve::editing::AuthorityPlan> preflight(
const eve::editing::TransactionSpec& transaction,
std::span<const eve::editing::DomainOperation> operations) override {
++preflights;
return local_.preflight(transaction, operations);
}
eve::editing::Result<eve::editing::TransactionReceipt> commit(
const eve::editing::AuthorityPlan& plan) override {
++commits;
return local_.commit(plan);
}
eve::editing::Result<eve::editing::TransactionReceipt> compensate(
const eve::editing::TransactionReceipt& receipt) override {
return local_.compensate(receipt);
}
int preflights = 0;
int commits = 0;
private:
eve::editing::LocalWorldAuthority local_;
};
class CountingOverlay final : public IEditorOverlay {
public:
void line(const OverlayPoint&, const OverlayPoint&, const OverlayStyle&) override {
++lines;
}
void circle(const OverlayPoint&, float, const OverlayStyle&) override { ++circles; }
void rectangle(const OverlayPoint&, const OverlayPoint&, const OverlayStyle&) override {}
void text(const OverlayPoint&, const std::string&, const OverlayStyle&) override {}
int lines = 0;
int circles = 0;
};
BuildingEdgeCurveToolSelection placedCurve(eve::building::PlacementWorld& world,
BuildingPlacementTarget& target) {
BuildingEdgeCurveToolSelection selection;
selection.buildingId = "editor-tool-fence";
selection.controlPoints = {{1.0, 1.0}, {1.0, 5.0}, {6.0, 5.0}, {6.0, 1.0}};
selection.subdivisions = 20;
auto operation = target.makeEdgeCubicBezier(selection.buildingId,
selection.controlPoints,
selection.subdivisions);
REQUIRE(operation.ok());
REQUIRE(target.applyDomainOperation(operation.value()).ok());
selection.memberInstanceId = world.getBuildingInstanceAt(0);
return selection;
}
EditorPointerEvent pointer(EditorPointerEvent::Phase phase, float x, float y,
int pointerId = 4) {
EditorPointerEvent event;
event.phase = phase;
event.pointerId = pointerId;
event.button = 0;
event.x = x;
event.y = y;
return event;
}
} // namespace
TEST_CASE("editor.building.curve_tool_routes_drag_as_one_authority_transaction") {
registerToolFence();
eve::building::PlacementWorld world(12, 12, 10.f);
world.setGridPlane("xz");
BuildingPlacementTarget target("building-curve-tool-world", &world);
BuildingEdgeCurveToolSelection selection = placedCurve(world, target);
const auto groupBefore = world.edgeCurveGroupForInstance(selection.memberInstanceId);
REQUIRE(groupBefore.ok());
const auto revisionBefore = target.revision();
TestBuildingViewport viewport;
CountingAuthority authority(&target);
BuildingEdgeCurveTool tool(&viewport, &authority);
REQUIRE(tool.setSelection(selection).ok());
eve::editor::EditorSession session;
session.bindTarget(target);
REQUIRE(session.addTool(&tool));
REQUIRE(session.activateTool(tool.descriptor().id));
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Down, 10.f, 10.f))
.capturePointer);
CHECK(session.hasPointerCapture());
CHECK(tool.isDragging());
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Move, 20.f, 10.f))
.handled);
CHECK_EQ(authority.preflights, 0);
CHECK_EQ(authority.commits, 0);
CHECK_EQ(target.revision(), revisionBefore);
CountingOverlay overlay;
session.drawOverlay(overlay);
CHECK_EQ(overlay.circles, 4);
CHECK(overlay.lines > 4);
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Up, 20.f, 10.f))
.releasePointer);
CHECK(!session.hasPointerCapture());
CHECK(!tool.isDragging());
CHECK_EQ(authority.preflights, 1);
CHECK_EQ(authority.commits, 1);
CHECK_EQ(target.revision(), revisionBefore + 1);
REQUIRE(tool.lastReceipt().has_value());
const eve::editing::TransactionReceipt firstReceipt = *tool.lastReceipt();
auto groupAfter = world.edgeCurveGroup(groupBefore.value().id);
REQUIRE(groupAfter.ok());
CHECK(std::abs(groupAfter.value().controlPoints[0].x - 2.f) < 0.0001f);
CHECK_EQ(groupAfter.value().controlPoints[0].y, groupBefore.value().controlPoints[0].y);
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Down, 20.f, 10.f))
.capturePointer);
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Move, 30.f, 10.f))
.handled);
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Up, 30.f, 10.f))
.releasePointer);
CHECK_EQ(authority.preflights, 2);
CHECK_EQ(authority.commits, 2);
REQUIRE(tool.lastReceipt().has_value());
const eve::editing::TransactionReceipt secondReceipt = *tool.lastReceipt();
auto groupAfterSecondDrag = world.edgeCurveGroup(groupBefore.value().id);
REQUIRE(groupAfterSecondDrag.ok());
CHECK(std::abs(groupAfterSecondDrag.value().controlPoints[0].x - 3.f) < 0.0001f);
REQUIRE(authority.compensate(secondReceipt).ok());
REQUIRE(authority.compensate(firstReceipt).ok());
auto undone = world.edgeCurveGroup(groupBefore.value().id);
REQUIRE(undone.ok());
CHECK_EQ(undone.value(), groupBefore.value());
eve::building::BuildingRegistry::clear();
}
TEST_CASE("editor.building.curve_tool_cancel_and_miss_do_not_mutate") {
registerToolFence();
eve::building::PlacementWorld world(12, 12, 10.f);
world.setGridPlane("xz");
BuildingPlacementTarget target("building-curve-tool-cancel", &world);
BuildingEdgeCurveToolSelection selection = placedCurve(world, target);
const auto before = world.edgeCurveGroupForInstance(selection.memberInstanceId);
REQUIRE(before.ok());
const auto revisionBefore = target.revision();
TestBuildingViewport viewport;
CountingAuthority authority(&target);
BuildingEdgeCurveTool tool(&viewport, &authority);
REQUIRE(tool.setSelection(selection).ok());
eve::editor::EditorSession session;
session.bindTarget(target);
REQUIRE(session.addTool(&tool));
REQUIRE(session.activateTool(tool.descriptor().id));
CHECK(!session.dispatchPointer(pointer(EditorPointerEvent::Phase::Down, 200.f, 200.f))
.handled);
CHECK(!session.hasPointerCapture());
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Down, 10.f, 10.f))
.capturePointer);
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Move, 30.f, 10.f))
.handled);
CHECK(session.dispatchPointer(pointer(EditorPointerEvent::Phase::Cancel, 30.f, 10.f))
.releasePointer);
CHECK_EQ(authority.preflights, 0);
CHECK_EQ(authority.commits, 0);
CHECK_EQ(target.revision(), revisionBefore);
auto after = world.edgeCurveGroup(before.value().id);
REQUIRE(after.ok());
CHECK_EQ(after.value(), before.value());
eve::building::BuildingRegistry::clear();
}