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

702 lines
27 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "building/Building.h"
#include "building/BuildingDef.h"
#include "building/Ghost.h"
#include "building/PlacementSystem.h"
#include "building/PlacementWorld.h"
#include "building/fx/BuildingFx.h"
#include "common/ECS.h"
#include "graphics/RenderSystem3D.h"
#include "graphics/Mesh.h"
#include <cmath>
using namespace eve::building;
using namespace eve::buildingfx;
TEST_CASE("buildingfx.sync.lifecycle") {
BuildingRegistry::clear();
PlacementSystem::clearEvents();
PlacementSystem::ensureBuiltins();
BuildingDefinition hut;
hut.id = "hut";
hut.displayName = "Hut";
hut.footprintW = 2;
hut.footprintH = 1;
hut.renderMode = "2d";
hut.visual2d["colorR"] = "0.8";
hut.visual2d["colorG"] = "0.4";
hut.visual2d["colorB"] = "0.2";
BuildingRegistry::registerBuilding(hut);
PlacementWorld world(8, 8, 32.f);
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx != nullptr);
REQUIRE(fx->attach(&world));
REQUIRE(fx->isAttached(&world));
REQUIRE_EQ(fx->getVisualCount(&world), 0);
const int a = world.placeAt("hut", 1, 1, 0.f);
const int b = world.placeAt("hut", 4, 2, 0.f);
REQUIRE(a > 0);
REQUIRE(b > 0);
fx->sync(&world);
REQUIRE_EQ(fx->getVisualCount(&world), 2);
REQUIRE(world.removeBuilding(a));
fx->sync(&world);
REQUIRE_EQ(fx->getVisualCount(&world), 1);
Ghost ghost;
ghost.setBuildingId("hut");
ghost.setFromWorld(&world, 140.f, 80.f);
fx->updateGhost(&world, &ghost);
fx->hideGhost(&world);
fx->setGridVisible(&world, true);
REQUIRE(fx->getGridVisible(&world));
fx->setGridVisible(&world, false);
REQUIRE(!fx->getGridVisible(&world));
REQUIRE(fx->detach(&world));
REQUIRE(!fx->isAttached(&world));
REQUIRE_EQ(fx->getVisualCount(&world), 0);
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.surfacePoseUsesExactAnchorAndFrame") {
BuildingRegistry::clear();
PlacementSystem::ensureBuiltins();
BuildingDefinition ramp;
ramp.id = "ramp";
ramp.renderMode = "3d";
ramp.snapMode = "free";
BuildingRegistry::registerBuilding(ramp);
PlacementWorld world(8, 8, 1.f);
world.setGridPlane("xz");
PlacementSystem::registerSurfaceProvider(
"ramp.surface", [](const PlacementWorld &, float x, float y) {
PlacementSystem::PlacementHit hit;
hit.worldX = x;
hit.worldY = 1.5f;
hit.worldZ = y;
hit.normalY = 1.f;
hit.normalZ = 1.f;
hit.tangentX = 1.f;
hit.surfaceId = "ramp.mesh";
return eve::Result<PlacementSystem::PlacementHit>::success(std::move(hit));
});
Ghost ghost;
ghost.setBuildingId(ramp.id);
ghost.setFromSurface(&world, "ramp.surface", 2.25f, 3.75f);
REQUIRE(ghost.validate(&world));
REQUIRE(world.placeGhost(&ghost) > 0);
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->sync(&world);
auto view = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::Transform3D>();
int visualCount = 0;
for (auto it = view.begin(); it != view.end(); ++it) {
auto [transform] = *it;
REQUIRE(std::abs(transform->x - 2.25f) < 0.0001f);
REQUIRE(std::abs(transform->y - 1.5f) < 0.0001f);
REQUIRE(std::abs(transform->z - 3.75f) < 0.0001f);
REQUIRE(std::abs(transform->pitch - 0.7853982f) < 0.0001f);
++visualCount;
}
REQUIRE_EQ(visualCount, 1);
REQUIRE(fx->detach(&world));
PlacementSystem::unregisterSurface("ramp.surface");
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.edgeVisualUsesMidpointLengthAndThickness") {
BuildingRegistry::clear();
BuildingDefinition wall;
wall.id = "wall";
wall.placementKind = "edge";
wall.renderMode = "3d";
wall.visual3d["height"] = "3";
wall.visual3d["thickness"] = "0.5";
wall.visual3d["variant.end.height"] = "4";
BuildingRegistry::registerBuilding(wall);
PlacementWorld world(4, 4, 10.f);
world.setGridPlane("xz");
REQUIRE(world.placeEdge("wall", 1, 2, "north") > 0);
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->sync(&world);
int visualCount = 0;
{
auto view = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::Transform3D>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [transform] = *it;
REQUIRE(std::abs(transform->x - 15.f) < 0.0001f);
REQUIRE(std::abs(transform->z - 20.f) < 0.0001f);
REQUIRE(std::abs(transform->sx - 10.f) < 0.0001f);
REQUIRE(std::abs(transform->sy - 3.f) < 0.0001f);
REQUIRE(std::abs(transform->sz - 0.5f) < 0.0001f);
++visualCount;
}
}
REQUIRE_EQ(visualCount, 1);
REQUIRE(world.placeEdge("wall", 2, 2, "north") > 0);
fx->sync(&world);
REQUIRE_EQ(fx->getVisualCount(&world), 2);
visualCount = 0;
auto updatedView = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::Transform3D>();
for (auto it = updatedView.begin(); it != updatedView.end(); ++it) {
auto [transform] = *it;
REQUIRE(std::abs(transform->sy - 4.f) < 0.0001f);
++visualCount;
}
REQUIRE(visualCount >= 1);
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.edgeCurveRasterUsesAuthoritativeTopologyVisuals") {
BuildingRegistry::clear();
BuildingDefinition wall;
wall.id = "curve-visual-wall";
wall.placementKind = "edge";
wall.connectionGroup = "curve-visual-wall";
wall.channel = "walls";
wall.renderMode = "3d";
wall.visual3d["variant.corner.mesh"] = "wall/curve-corner.mesh";
BuildingRegistry::registerBuilding(wall);
PlacementWorld world(10, 10, 10.f);
world.setGridPlane("xz");
const std::vector<PlacementSystem::EdgeCurvePoint> controls{
{1.f, 1.f}, {1.f, 6.f}, {7.f, 6.f}, {7.f, 1.f}};
auto placed = PlacementSystem::placeEdgeCubicBezier(&world, wall.id, controls, 24);
REQUIRE(placed.ok());
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->sync(&world);
CHECK_EQ(fx->getVisualCount(&world),
static_cast<int>(placed.value().instanceIds.size()));
CHECK_EQ(fx->getCurveGroupCount(&world), 1);
CHECK_EQ(fx->getContinuousCurveVisualCount(&world), 0);
CHECK_EQ(fx->getCurveVisualFallbackReason(&world, placed.value().instanceIds.front()),
"graphics_unavailable");
int cornerCount = 0;
for (int instanceId : placed.value().instanceIds) {
if (fx->getVisualVariant(&world, instanceId) != "corner") continue;
CHECK_EQ(fx->getVisualResource(&world, instanceId), "wall/curve-corner.mesh");
++cornerCount;
}
CHECK(cornerCount > 0);
auto continuousMesh = BuildingFx::buildEdgeCurveGroupMeshForInstance(
world, placed.value().instanceIds.front(), 2.f, 3.f, 4.f);
REQUIRE(continuousMesh.ok());
CHECK_EQ(continuousMesh.value().sampleCount, 25);
REQUIRE(world.removeBuilding(placed.value().instanceIds.front()));
fx->sync(&world);
CHECK_EQ(fx->getCurveGroupCount(&world), 0);
CHECK_EQ(fx->getContinuousCurveVisualCount(&world), 0);
CHECK_EQ(fx->getCurveVisualFallbackReason(&world, placed.value().instanceIds.back()),
"curve_group_not_found");
CHECK(!BuildingFx::buildEdgeCurveGroupMeshForInstance(
world, placed.value().instanceIds.back(), 2.f, 3.f, 4.f)
.ok());
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.edgeCurveMeshBuildsContinuousWorldSpaceExtrusion") {
PlacementWorld world(10, 10, 10.f);
world.setOrigin(3.f, 7.f);
world.setGridPlane("xz");
const std::vector<BuildingFx::CurveControlPoint> controls{
{1.f, 1.f}, {1.f, 6.f}, {7.f, 6.f}, {7.f, 1.f}};
auto mesh = BuildingFx::buildEdgeCurveMesh(world, controls, 24, 2.f, 3.f, 4.f);
REQUIRE(mesh.ok());
CHECK_EQ(mesh.value().positions.size(), size_t{((24 + 1) * 8 + 8) * 3});
CHECK_EQ(mesh.value().normals.size(), mesh.value().positions.size());
CHECK_EQ(mesh.value().uvs.size(), size_t{((24 + 1) * 8 + 8) * 2});
CHECK_EQ(mesh.value().indices.size(), size_t{24 * 24 + 12});
CHECK_EQ(mesh.value().sampleCount, 25);
CHECK(mesh.value().length > 60.f);
for (size_t index : {size_t{0}, size_t{3}, size_t{mesh.value().positions.size() - 1}})
CHECK(std::isfinite(mesh.value().positions[index]));
CHECK(!BuildingFx::buildEdgeCurveMesh(world, controls, 1, 2.f, 3.f, 4.f).ok());
CHECK(!BuildingFx::buildEdgeCurveMesh(world, controls, 24, 0.f, 3.f, 4.f).ok());
PlacementWorld xyWorld(10, 10, 10.f);
xyWorld.setGridPlane("xy");
auto xyMesh = BuildingFx::buildEdgeCurveMesh(xyWorld, controls, 8, 1.f, 2.f, 5.f);
REQUIRE(xyMesh.ok());
CHECK_EQ(xyMesh.value().sampleCount, 9);
CHECK(std::abs(xyMesh.value().positions[2] - 5.f) < 0.0001f);
CHECK(std::abs(xyMesh.value().positions[5] - 7.f) < 0.0001f);
PlacementWorld hexWorld(10, 10, 10.f);
hexWorld.setGridLayout("hexagon");
CHECK(!BuildingFx::buildEdgeCurveMesh(hexWorld, controls, 8, 1.f, 2.f, 0.f).ok());
}
TEST_CASE("buildingfx.edgeCurvePreviewOwnsCpuMeshAndReportsBackendFallback") {
BuildingRegistry::clear();
BuildingDefinition wall;
wall.id = "curve-preview-wall";
wall.placementKind = "edge";
wall.channel = "walls";
wall.renderMode = "3d";
wall.visual3d["thickness"] = "0.5";
wall.visual3d["height"] = "3";
BuildingRegistry::registerBuilding(wall);
PlacementWorld world(10, 10, 10.f);
world.setGridPlane("xz");
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
const std::vector<BuildingFx::CurveControlPoint> controls{
{1.f, 1.f}, {1.f, 6.f}, {7.f, 6.f}, {7.f, 1.f}};
auto updated =
fx->updateEdgeCurvePreview(&world, wall.id, controls, 24, 2);
REQUIRE(updated.ok());
CHECK(fx->hasEdgeCurvePreview(&world));
CHECK_EQ(fx->getEdgeCurvePreviewFallbackReason(&world),
"graphics_unavailable");
fx->clearEdgeCurvePreview(&world);
CHECK(!fx->hasEdgeCurvePreview(&world));
CHECK_EQ(fx->getEdgeCurvePreviewFallbackReason(&world),
"curve_preview_not_active");
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.edgeCurveCommitsAndExtrudesCustomSurfaceFrames") {
BuildingRegistry::clear();
BuildingDefinition wall;
wall.id = "surface-curve-wall";
wall.placementKind = "edge";
wall.channel = "walls";
wall.renderMode = "3d";
BuildingRegistry::registerBuilding(wall);
PlacementWorld world(12, 12, 10.f);
world.setGridPlane("xz");
PlacementSystem::registerSurfaceProvider(
"curve.terrain", [](const PlacementWorld &, float x, float y) {
PlacementSystem::PlacementHit hit;
hit.worldX = x;
hit.worldY = x * 0.1f;
hit.worldZ = y;
hit.normalX = -0.1f;
hit.normalY = 1.f;
hit.surfaceId = "terrain.curve";
hit.surfaceRevision = 12;
return eve::Result<PlacementSystem::PlacementHit>::success(std::move(hit));
});
const std::vector<PlacementSystem::EdgeCurvePoint> controls{
{1.f, 1.f}, {1.f, 6.f}, {7.f, 6.f}, {7.f, 1.f}};
auto placed = PlacementSystem::placeEdgeCubicBezierOnSurface(
&world, wall.id, controls, 16, "curve.terrain");
REQUIRE(placed.ok());
auto group = world.edgeCurveGroupForInstance(placed.value().instanceIds.front());
REQUIRE(group.ok());
CHECK_EQ(group.value().surfaceProviderName, "curve.terrain");
CHECK_EQ(group.value().surfaceId, "terrain.curve");
CHECK_EQ(group.value().surfaceRevision, uint64_t{12});
CHECK_EQ(group.value().surfaceSamples.size(), size_t{17});
auto mesh = BuildingFx::buildEdgeCurveGroupMeshForInstance(
world, placed.value().instanceIds.front(), 1.f, 3.f);
REQUIRE(mesh.ok());
CHECK_EQ(mesh.value().sampleCount, 17);
CHECK(mesh.value().positions[1] <
mesh.value().positions[mesh.value().positions.size() - 23]);
PlacementWorld rejectedWorld(12, 12, 10.f);
rejectedWorld.setGridPlane("xz");
PlacementSystem::registerSurfaceProvider(
"curve.split", [](const PlacementWorld &, float x, float y) {
PlacementSystem::PlacementHit hit;
hit.worldX = x;
hit.worldY = 0.f;
hit.worldZ = y;
hit.surfaceId = x < 40.f ? "terrain.left" : "terrain.right";
hit.surfaceRevision = 1;
return eve::Result<PlacementSystem::PlacementHit>::success(std::move(hit));
});
CHECK(!PlacementSystem::placeEdgeCubicBezierOnSurface(
&rejectedWorld, wall.id, controls, 16, "curve.split")
.ok());
CHECK_EQ(rejectedWorld.getBuildingCount(), 0);
CHECK_EQ(rejectedWorld.getEdgeCurveGroupCount(), 0);
PlacementSystem::unregisterSurface("curve.terrain");
PlacementSystem::unregisterSurface("curve.split");
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.sessionCurvePreviewFollowsSurfaceAndKeepsLastValidDraft") {
BuildingRegistry::clear();
BuildingDefinition wall;
wall.id = "session-surface-preview-wall";
wall.placementKind = "edge";
wall.channel = "walls";
wall.renderMode = "3d";
wall.visual3d["thickness"] = "0.5";
wall.visual3d["height"] = "3";
BuildingRegistry::registerBuilding(wall);
PlacementWorld world(10, 10, 10.f);
world.setGridPlane("xz");
PlacementSystem::registerSurfaceProvider(
"preview.terrain", [](const PlacementWorld &, float x, float y) {
PlacementSystem::PlacementHit hit;
hit.worldX = x;
hit.worldY = x * 0.1f;
hit.worldZ = y;
hit.normalX = -0.1f;
hit.normalY = 1.f;
hit.surfaceId = "terrain.preview";
hit.surfaceRevision = 8;
return eve::Result<PlacementSystem::PlacementHit>::success(std::move(hit));
});
PlacementSession session;
REQUIRE(session.startPlacement(&world, wall.id));
REQUIRE(static_cast<int>(session.beginEdgeCurve(1.f, 1.f)) ==
static_cast<int>(EdgePathUpdateStatus::Updated));
REQUIRE(static_cast<int>(session.appendEdgeCurveControlPoint(1.f, 6.f)) ==
static_cast<int>(EdgePathUpdateStatus::Updated));
REQUIRE(static_cast<int>(session.appendEdgeCurveControlPoint(7.f, 6.f)) ==
static_cast<int>(EdgePathUpdateStatus::Updated));
REQUIRE(static_cast<int>(session.appendEdgeCurveControlPoint(7.f, 1.f)) ==
static_cast<int>(EdgePathUpdateStatus::Updated));
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
REQUIRE(fx->updateEdgeCurveSurfacePreview(&world, &session, 16, "preview.terrain").ok());
CHECK(fx->hasEdgeCurvePreview(&world));
CHECK_EQ(fx->getEdgeCurvePreviewSurfaceId(&world), "terrain.preview");
CHECK_EQ(fx->getEdgeCurvePreviewSurfaceRevision(&world), uint64_t{8});
CHECK(!fx->updateEdgeCurveSurfacePreview(&world, &session, 16, "missing.provider").ok());
CHECK_EQ(fx->getEdgeCurvePreviewSurfaceId(&world), "terrain.preview");
CHECK_EQ(fx->getEdgeCurvePreviewSurfaceRevision(&world), uint64_t{8});
REQUIRE(fx->detach(&world));
PlacementSystem::unregisterSurface("preview.terrain");
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.cornerVisualUsesVertexAnchorAndExplicitSize") {
BuildingRegistry::clear();
BuildingDefinition post;
post.id = "visual-post";
post.placementKind = "corner";
post.channel = "posts";
post.renderMode = "3d";
post.visual3d["width"] = "0.25";
post.visual3d["depth"] = "0.35";
post.visual3d["height"] = "3";
BuildingRegistry::registerBuilding(post);
PlacementWorld world(4, 4, 10.f);
world.setGridPlane("xz");
REQUIRE(world.placeCorner(post.id, 2, 3) > 0);
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->sync(&world);
int visualCount = 0;
auto view = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::Transform3D>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [transform] = *it;
CHECK(std::abs(transform->x - 20.f) < 0.0001f);
CHECK(std::abs(transform->z - 30.f) < 0.0001f);
CHECK(std::abs(transform->sx - 0.25f) < 0.0001f);
CHECK(std::abs(transform->sy - 3.f) < 0.0001f);
CHECK(std::abs(transform->sz - 0.35f) < 0.0001f);
++visualCount;
}
CHECK_EQ(visualCount, 1);
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.freeVisualUsesExactAnchorAndCollisionDiameterFallback") {
BuildingRegistry::clear();
BuildingDefinition prop;
prop.id = "visual-free-prop";
prop.placementKind = "free";
prop.snapMode = "free";
prop.rotationMode = "free";
prop.freeRadiusCells = 0.3f;
prop.renderMode = "3d";
prop.visual3d["height"] = "2";
BuildingRegistry::registerBuilding(prop);
PlacementWorld world(4, 4, 10.f);
world.setGridPlane("xz");
REQUIRE(world.placeFree(prop.id, 12.5f, 23.75f, 1.f, 27.f) > 0);
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->sync(&world);
int count = 0;
auto view = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::Transform3D>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [transform] = *it;
CHECK(std::abs(transform->x - 12.5f) < 0.0001f);
CHECK(std::abs(transform->y - 1.f) < 0.0001f);
CHECK(std::abs(transform->z - 23.75f) < 0.0001f);
CHECK(std::abs(transform->sx - 6.f) < 0.0001f);
CHECK(std::abs(transform->sz - 6.f) < 0.0001f);
++count;
}
CHECK_EQ(count, 1);
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.freeCustomVisualUsesCommittedFootprintBounds") {
BuildingRegistry::clear();
BuildingDefinition prop;
prop.id = "visual-free-obb";
prop.placementKind = "free";
prop.snapMode = "free";
prop.rotationMode = "free";
prop.freeFootprintWidthCells = 2.f;
prop.freeFootprintHeightCells = 0.5f;
prop.freeFootprintVertices = {-1.f, 0.f, 0.f, -0.25f,
1.f, 0.f, 0.f, 0.25f};
prop.renderMode = "3d";
BuildingRegistry::registerBuilding(prop);
PlacementWorld world(4, 4, 10.f);
world.setGridPlane("xz");
REQUIRE(world.placeFree(prop.id, 12.5f, 23.75f, 1.f, 27.f) > 0);
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->sync(&world);
int count = 0;
auto view = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::Transform3D>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [transform] = *it;
CHECK(std::abs(transform->sx - 20.f) < 0.0001f);
CHECK(std::abs(transform->sz - 5.f) < 0.0001f);
++count;
}
CHECK_EQ(count, 1);
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.edgeTopologySelectsMaskClassAndBaseResources") {
BuildingRegistry::clear();
BuildingDefinition wall;
wall.id = "resource-wall";
wall.placementKind = "edge";
wall.connectionGroup = "resource-boundary";
wall.channel = "boundary";
wall.renderMode = "3d";
wall.visual3d["mesh"] = "wall/base.mesh";
wall.visual3d["variant.end.mesh"] = "wall/end.mesh";
wall.visual3d["variant.end.rotationDeg"] = "15";
wall.visual3d["variant.corner.mesh"] = "wall/corner.mesh";
wall.visual3d["variant.corner.mask.34.mesh"] = "wall/corner-34.mesh";
wall.visual3d["variant.tee.mesh"] = "wall/tee.mesh";
wall.visual3d["variant.cross.mesh"] = "wall/cross.mesh";
BuildingRegistry::registerBuilding(wall);
struct Case {
int mask;
const char *variant;
const char *resource;
};
const Case cases[] = {{0, "isolated", "wall/base.mesh"},
{1, "end", "wall/end.mesh"},
{3, "straight", "wall/base.mesh"},
{34, "corner", "wall/corner-34.mesh"},
{42, "tee", "wall/tee.mesh"},
{43, "cross", "wall/cross.mesh"}};
BuildingFx *fx = BuildingFx::create();
for (const Case &test : cases) {
PlacementWorld world(8, 8, 10.f);
world.setGridPlane("xz");
const int center = world.placeEdge(wall.id, 2, 2, "north");
REQUIRE(center > 0);
if (test.mask & 0x01) REQUIRE(world.placeEdge(wall.id, 1, 2, "north") > 0);
if (test.mask & 0x02) REQUIRE(world.placeEdge(wall.id, 3, 2, "north") > 0);
if (test.mask & 0x04) REQUIRE(world.placeEdge(wall.id, 2, 1, "west") > 0);
if (test.mask & 0x08) REQUIRE(world.placeEdge(wall.id, 2, 2, "west") > 0);
if (test.mask & 0x10) REQUIRE(world.placeEdge(wall.id, 3, 1, "west") > 0);
if (test.mask & 0x20) REQUIRE(world.placeEdge(wall.id, 3, 2, "west") > 0);
REQUIRE_EQ(world.getEdgeConnectionMask(center), test.mask);
REQUIRE(fx->attach(&world));
fx->sync(&world);
CHECK_EQ(fx->getVisualVariant(&world, center), test.variant);
CHECK_EQ(fx->getVisualResource(&world, center), test.resource);
CHECK_EQ(fx->getVisualFallbackReason(&world, center), "resolver_unavailable");
REQUIRE(fx->detach(&world));
}
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.edgeTopologyRefreshesResourceRotationAndMirror") {
BuildingRegistry::clear();
BuildingDefinition wall;
wall.id = "dynamic-wall";
wall.placementKind = "edge";
wall.renderMode = "3d";
wall.visual3d["mesh"] = "wall/base.mesh";
wall.visual3d["variant.end.mesh"] = "wall/end.mesh";
wall.visual3d["variant.end.rotationDeg"] = "15";
BuildingRegistry::registerBuilding(wall);
PlacementWorld world(8, 8, 10.f);
world.setGridPlane("xz");
const int center = world.placeEdge(wall.id, 2, 2, "north");
REQUIRE(center > 0);
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->sync(&world);
CHECK_EQ(fx->getVisualVariant(&world, center), "isolated");
CHECK_EQ(fx->getVisualResource(&world, center), "wall/base.mesh");
REQUIRE(world.placeEdge(wall.id, 1, 2, "north") > 0);
fx->sync(&world);
CHECK_EQ(fx->getVisualVariant(&world, center), "end");
CHECK_EQ(fx->getVisualResource(&world, center), "wall/end.mesh");
bool foundCenter = false;
auto view = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::Transform3D>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [transform] = *it;
if (std::abs(transform->x - 25.f) > 0.0001f ||
std::abs(transform->z - 20.f) > 0.0001f)
continue;
CHECK(transform->sx < 0.f);
CHECK(std::abs(transform->yaw - 0.2617994f) < 0.0001f);
foundCenter = true;
}
REQUIRE(foundCenter);
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.meshResolverPublishesBorrowedTopologyResource") {
BuildingRegistry::clear();
BuildingDefinition wall;
wall.id = "resolved-wall";
wall.placementKind = "edge";
wall.renderMode = "3d";
wall.visual3d["mesh"] = "wall/resolved.mesh";
BuildingRegistry::registerBuilding(wall);
PlacementWorld world(4, 4, 1.f);
const int instanceId = world.placeEdge(wall.id, 1, 1, "north");
REQUIRE(instanceId > 0);
eve::graphics::Mesh mesh;
int resolverCalls = 0;
BuildingFx *fx = BuildingFx::create();
fx->setMeshResolver([&](const std::string &resourceId) {
++resolverCalls;
return resourceId == "wall/resolved.mesh" ? &mesh : nullptr;
});
REQUIRE(fx->attach(&world));
fx->sync(&world);
CHECK(resolverCalls > 0);
CHECK_EQ(fx->getVisualResource(&world, instanceId), "wall/resolved.mesh");
CHECK(fx->getVisualFallbackReason(&world, instanceId).empty());
bool usesResolvedMesh = false;
auto view = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::MeshRenderer>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [renderer] = *it;
if (renderer->mesh == &mesh) usesResolvedMesh = true;
}
CHECK(usesResolvedMesh);
REQUIRE(fx->detach(&world));
fx->clearMeshResolver();
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.areaPreviewProjection") {
BuildingRegistry::clear();
BuildingDefinition tile;
tile.id = "heat-tile";
BuildingRegistry::registerBuilding(tile);
PlacementWorld world(4, 4, 1.f);
REQUIRE(world.placeAt(tile.id, 1, 1) > 0);
PlacementSession session;
REQUIRE(session.startPlacement(&world, tile.id));
CHECK_EQ(static_cast<int>(session.previewRectangle(0, 1, 2, 1)),
static_cast<int>(AreaExecuteStatus::Rejected));
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->updateAreaPreview(&world, &session);
CHECK_EQ(fx->getAreaPreviewCount(&world), 3);
CHECK(fx->getAreaPreviewAccepted(&world, 0));
CHECK(!fx->getAreaPreviewAccepted(&world, 1));
fx->clearAreaPreview(&world);
CHECK_EQ(fx->getAreaPreviewCount(&world), 0);
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}
TEST_CASE("buildingfx.projectsDiscreteFloorAlongGridNormal") {
BuildingRegistry::clear();
BuildingDefinition marker;
marker.id = "floor-marker";
marker.renderMode = "3d";
BuildingRegistry::registerBuilding(marker);
PlacementWorld world(4, 4, 1.f);
world.setGridPlane("xz");
world.setFloorHeight(3.25f);
const int groundId = world.placeAt(marker.id, 2, 1);
REQUIRE(groundId > 0);
world.setActiveLevel(2);
const int instanceId = world.placeAt(marker.id, 2, 1);
REQUIRE(instanceId > 0);
BuildingFx *fx = BuildingFx::create();
REQUIRE(fx->attach(&world));
fx->sync(&world);
bool found = false;
auto view = ecs::View<eve::graphics::Renderable3D,
eve::graphics::Renderable3D::Transform3D>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [transform] = *it;
if (std::abs(transform->x - 2.f) < 0.0001f &&
std::abs(transform->z - 1.f) < 0.0001f &&
std::abs(transform->y - 6.5f) < 0.0001f)
found = true;
}
CHECK(found);
CHECK_EQ(fx->getLevelVisibilityMode(&world), "all");
CHECK(fx->isVisualVisible(&world, groundId));
CHECK(fx->isVisualVisible(&world, instanceId));
fx->setLevelVisibilityMode(&world, "active");
CHECK(!fx->isVisualVisible(&world, groundId));
CHECK(fx->isVisualVisible(&world, instanceId));
world.setActiveLevel(0);
fx->sync(&world);
CHECK(fx->isVisualVisible(&world, groundId));
CHECK(!fx->isVisualVisible(&world, instanceId));
fx->setLevelVisibilityMode(&world, "active_and_below");
CHECK(fx->isVisualVisible(&world, groundId));
CHECK(!fx->isVisualVisible(&world, instanceId));
world.setActiveLevel(2);
fx->sync(&world);
CHECK(fx->isVisualVisible(&world, groundId));
CHECK(fx->isVisualVisible(&world, instanceId));
REQUIRE(fx->detach(&world));
BuildingRegistry::clear();
}