Files
西风逍遥游 7820703cfa feat(building): 统一 Grid 抽象 + 3D 放置 + 多样式 Tilemap 打通
对照 Unity Grid/Tilemap 与 Grid Placement System 架构补齐建筑放置能力:

- 新增 grid 模块:GridConfig(rectangle/hexagon/isometric/staggered/iso-z-as-y、cellGap、stagger、XY/XZ 平面轴)+ 投影/占地旋转纯函数;map::TileProjection 委托复用,公开 API 不变。
- PlacementWorld 支持可选 GridConfig、多通道占用(channel)、bindTileLayer + GID 地形懒解析、XZ 平面 3D 放置、surface 表面接口(内置 plane)。
- BuildingDefinition 新增 channel/renderMode/visual2d/visual3d;rotationMode 支持 hex(60°)。
- 新增 buildingfx 渲染桥:按定义生成/同步 Renderable2D/3D、鬼影+占地光标、2D/3D 网格可视化。
- 新增 PlacementSession(放置/拆除/旋转/指针更新)。
- 测试:grid/building/buildingfx 新用例(REQUIRE 级)+ map/sceneloader 回归;示例 building-tilemap(等距/六角)、building-3d(XZ 地面)。
- 顺带修复 HEAD 上 sceneloader 未迁移 SceneLink 新 API 导致的编译失败(SceneLoader.cpp / test/sceneloader.cpp)。
2026-08-18 17:41:01 +08:00

170 lines
5.0 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "grid/GridConfig.h"
#include "grid/GridProjection.h"
#include <cmath>
#include <cstdint>
#include <cstring>
#include <utility>
#include <vector>
using namespace eve::grid;
namespace {
bool approxEq(float a, float b, float eps = 1e-4f) { return std::fabs(a - b) < eps; }
void expectCellWorld(const GridConfig &cfg, int cx, int cy, float ex, float ey) {
float wx = 0.f, wy = 0.f;
cellToWorld(cfg, cx, cy, wx, wy);
REQUIRE(approxEq(wx, ex));
REQUIRE(approxEq(wy, ey));
int rx = -999, ry = -999;
worldToCell(cfg, wx + cfg.cellW * 0.25f, wy + cfg.cellH * 0.25f, rx, ry, 32, 32);
REQUIRE_EQ(rx, cx);
REQUIRE_EQ(ry, cy);
}
} // namespace
TEST_CASE("grid.rectangle.roundtrip") {
GridConfig cfg;
cfg.cellW = 32.f;
cfg.cellH = 16.f;
cfg.originX = 10.f;
cfg.originY = 20.f;
expectCellWorld(cfg, 0, 0, 10.f, 20.f);
expectCellWorld(cfg, 3, 2, 10.f + 3 * 32.f, 20.f + 2 * 16.f);
cfg.cellGapX = 4.f;
cfg.cellGapY = 2.f;
expectCellWorld(cfg, 1, 1, 10.f + 36.f, 20.f + 18.f);
}
TEST_CASE("grid.isometric.roundtrip") {
GridConfig cfg;
cfg.layout = GridLayout::Isometric;
cfg.cellW = 64.f;
cfg.cellH = 32.f;
cfg.originX = 0.f;
cfg.originY = 0.f;
expectCellWorld(cfg, 0, 0, 0.f, 0.f);
expectCellWorld(cfg, 1, 0, 32.f, 16.f);
expectCellWorld(cfg, 0, 1, -32.f, 16.f);
expectCellWorld(cfg, 2, 3, -32.f, 80.f);
}
TEST_CASE("grid.staggered.cellToWorld") {
GridConfig cfg;
cfg.layout = GridLayout::Staggered;
cfg.cellW = 32.f;
cfg.cellH = 32.f;
cfg.staggerAxis = StaggerAxis::Y;
cfg.staggerIndex = StaggerIndex::Odd;
float wx = 0.f, wy = 0.f;
cellToWorld(cfg, 0, 0, wx, wy);
REQUIRE(approxEq(wx, 0.f));
REQUIRE(approxEq(wy, 0.f));
// 奇行向右偏移半格。
cellToWorld(cfg, 0, 1, wx, wy);
REQUIRE(approxEq(wx, 16.f));
REQUIRE(approxEq(wy, 16.f));
cellToWorld(cfg, 1, 2, wx, wy);
REQUIRE(approxEq(wx, 32.f));
REQUIRE(approxEq(wy, 32.f));
int cx = -1, cy = -1;
worldToCell(cfg, 60.f, 30.f, cx, cy, 8, 8);
REQUIRE_EQ(cx, 1);
REQUIRE_EQ(cy, 1);
}
TEST_CASE("grid.hex.cellToWorldDepth") {
GridConfig cfg;
cfg.layout = GridLayout::Hexagon;
cfg.cellW = 32.f;
cfg.cellH = 32.f;
cfg.hexSideLength = 14.f;
cfg.staggerAxis = StaggerAxis::Y;
cfg.staggerIndex = StaggerIndex::Odd;
float wx = 0.f, wy = 0.f;
cellToWorld(cfg, 0, 0, wx, wy);
REQUIRE(approxEq(wx, 0.f));
REQUIRE(approxEq(wy, 0.f));
cellToWorld(cfg, 0, 1, wx, wy);
REQUIRE(approxEq(wx, 16.f));
REQUIRE(approxEq(wy, (32.f + 14.f) * 0.5f));
REQUIRE(approxEq(cellToDepthY(cfg, 0, 1), wy + 32.f));
}
TEST_CASE("grid.footprint.cardinalRotate") {
std::vector<uint8_t> mask; // 实心 2x1
std::vector<std::pair<int, int>> cells;
foreachRotatedFootprint(2, 1, mask, 1, false, [&](int lx, int ly) {
cells.emplace_back(lx, ly);
});
// 90° 后变为 1x2:{(0,0),(0,1)}
REQUIRE_EQ(cells.size(), size_t(2));
const bool has00 = (cells[0].first == 0 && cells[0].second == 0) ||
(cells[1].first == 0 && cells[1].second == 0);
const bool has01 = (cells[0].first == 0 && cells[0].second == 1) ||
(cells[1].first == 0 && cells[1].second == 1);
REQUIRE(has00);
REQUIRE(has01);
cells.clear();
foreachRotatedFootprint(2, 1, mask, 4, false, [&](int lx, int ly) {
cells.emplace_back(lx, ly);
});
// 360° = 恒等
REQUIRE_EQ(cells.size(), size_t(2));
REQUIRE_EQ(cells[0].first, 0);
REQUIRE_EQ(cells[0].second, 0);
REQUIRE_EQ(cells[1].first, 1);
REQUIRE_EQ(cells[1].second, 0);
}
TEST_CASE("grid.footprint.hexRotate") {
std::vector<uint8_t> mask; // 实心 2x1
std::vector<std::pair<int, int>> cells;
foreachRotatedFootprint(2, 1, mask, 1, true, [&](int lx, int ly) {
cells.emplace_back(lx, ly);
});
// 60° 旋转 2x1 在 offset 空间产生 2 个格(不再是 1x2 轴对齐)。
REQUIRE_EQ(cells.size(), size_t(2));
cells.clear();
foreachRotatedFootprint(2, 1, mask, 6, true, [&](int lx, int ly) {
cells.emplace_back(lx, ly);
});
REQUIRE_EQ(cells.size(), size_t(2));
REQUIRE_EQ(cells[0].first, 0);
REQUIRE_EQ(cells[0].second, 0);
REQUIRE_EQ(cells[1].first, 1);
REQUIRE_EQ(cells[1].second, 0);
int w = 0, h = 0;
rotatedFootprintSize(2, 1, mask, 3, true, w, h);
const bool sizeOk = w >= 1 && h >= 1;
REQUIRE(sizeOk);
}
TEST_CASE("grid.layoutNames") {
const bool nameOk =
std::strcmp(GridConfig::layoutName(GridLayout::Isometric), "isometric") == 0;
REQUIRE(nameOk);
REQUIRE_EQ(int(GridConfig::layoutFromName("hex")), int(GridLayout::Hexagon));
REQUIRE_EQ(int(GridConfig::layoutFromName("bogus")), int(GridLayout::Rectangle));
REQUIRE_EQ(int(GridConfig::planeFromName("xz")), int(GridPlane::XZ));
const bool planeOk = std::strcmp(GridConfig::planeName(GridPlane::XY), "xy") == 0;
REQUIRE(planeOk);
}