feat(editor): rotate terrain 90 degrees clockwise

- Rotate 90 CW button in Mirror/Rotate section
- Snapshots outer vertex heights into 129x129 grid, rotates in-place,
  writes back with inner vertices averaged from surrounding outers
- Useful for reorienting terrain features or creating rotational
  symmetry (rotate + mirror for 4-way symmetric arenas)
This commit is contained in:
Kelsi 2026-05-05 08:32:59 -07:00
parent 66b6404d25
commit 14d305a477
3 changed files with 53 additions and 1 deletions

View file

@ -627,7 +627,11 @@ void EditorUI::renderBrushPanel(EditorApp& app) {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1), "Set start then end to apply");
}
if (ImGui::CollapsingHeader("Mirror Terrain")) {
if (ImGui::CollapsingHeader("Mirror / Rotate")) {
if (ImGui::Button("Rotate 90 CW", ImVec2(-1, 0))) {
app.getTerrainEditor().rotateTerrain90();
app.showToast("Terrain rotated 90 degrees");
}
if (ImGui::Button("Mirror X (Left<>Right)", ImVec2(-1, 0))) {
app.getTerrainEditor().mirrorX();
app.showToast("Terrain mirrored X");

View file

@ -900,6 +900,51 @@ void TerrainEditor::applyVoronoiNoise(int cellCount, float amplitude, uint32_t s
dirty_ = true;
}
void TerrainEditor::rotateTerrain90() {
if (!terrain_) return;
// Snapshot all outer vertex heights into a 129x129 grid
std::array<std::array<float, 129>, 129> grid{};
for (int cy = 0; cy < 16; cy++) {
for (int cx = 0; cx < 16; cx++) {
auto& chunk = terrain_->chunks[cy * 16 + cx];
if (!chunk.hasHeightMap()) continue;
for (int v = 0; v < 145; v++) {
int row = v / 17, col = v % 17;
if (col > 8) continue;
grid[cy * 8 + row][cx * 8 + col] = chunk.heightMap.heights[v];
}
}
}
// Rotate 90 degrees CW: new[x][128-y] = old[y][x]
std::array<std::array<float, 129>, 129> rotated{};
for (int y = 0; y < 129; y++)
for (int x = 0; x < 129; x++)
rotated[x][128 - y] = grid[y][x];
// Write back
for (int cy = 0; cy < 16; cy++) {
for (int cx = 0; cx < 16; cx++) {
auto& chunk = terrain_->chunks[cy * 16 + cx];
if (!chunk.hasHeightMap()) continue;
for (int v = 0; v < 145; v++) {
int row = v / 17, col = v % 17;
if (col > 8) {
// Inner vertex: average of surrounding outer
int innerCol = col - 9;
int gy = cy * 8 + row, gx = cx * 8 + innerCol;
if (gy < 128 && gx < 128)
chunk.heightMap.heights[v] = (rotated[gy][gx] + rotated[gy][gx+1] +
rotated[gy+1][gx] + rotated[gy+1][gx+1]) * 0.25f;
} else {
chunk.heightMap.heights[v] = rotated[cy * 8 + row][cx * 8 + col];
}
}
dirtyChunks_.push_back(cy * 16 + cx);
}
}
for (int ci = 0; ci < 256; ci++) stitchEdges(ci);
dirty_ = true;
}
void TerrainEditor::offsetHeights(float amount) {
if (!terrain_) return;
for (int ci = 0; ci < 256; ci++) {

View file

@ -126,6 +126,9 @@ public:
// Add random detail noise to existing terrain (preserves shape, adds roughness)
void addDetailNoise(float amplitude, float frequency, uint32_t seed);
// Rotate terrain 90 degrees clockwise
void rotateTerrain90();
// Import/export heightmap (raw 16-bit grayscale, 129x129)
bool importHeightmap(const std::string& path, float heightScale);
bool exportHeightmap(const std::string& path, float heightScale);