From dd2b9294b5b0298f9c2a37b53b74e9525399628c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 06:24:28 -0700 Subject: [PATCH] feat(editor): terrain mirror X/Y for symmetric zone design - Mirror X: copies left half of terrain to right half (mirrored) - Mirror Y: copies top half to bottom half (mirrored) - Useful for creating symmetric zones, arenas, or balanced landscapes - Auto-stitches all chunk edges after mirror for seamless results - UI buttons in Sculpt panel under "Mirror Terrain" section --- tools/editor/editor_ui.cpp | 11 ++++++++ tools/editor/terrain_editor.cpp | 46 +++++++++++++++++++++++++++++++++ tools/editor/terrain_editor.hpp | 4 +++ 3 files changed, 61 insertions(+) diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index 020aeb66..27715a50 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -459,6 +459,17 @@ void EditorUI::renderBrushPanel(EditorApp& app) { } ImGui::Separator(); + if (ImGui::CollapsingHeader("Mirror Terrain")) { + if (ImGui::Button("Mirror X (Left<>Right)", ImVec2(-1, 0))) { + app.getTerrainEditor().mirrorX(); + app.showToast("Terrain mirrored X"); + } + if (ImGui::Button("Mirror Y (Top<>Bottom)", ImVec2(-1, 0))) { + app.getTerrainEditor().mirrorY(); + app.showToast("Terrain mirrored Y"); + } + } + if (ImGui::CollapsingHeader("Stamp / Clone")) { auto& brush2 = app.getTerrainEditor().brush(); if (ImGui::Button("Copy Stamp", ImVec2(120, 0)) && brush2.isActive()) diff --git a/tools/editor/terrain_editor.cpp b/tools/editor/terrain_editor.cpp index 66f6ae57..696f8127 100644 --- a/tools/editor/terrain_editor.cpp +++ b/tools/editor/terrain_editor.cpp @@ -666,6 +666,52 @@ void TerrainEditor::scaleHeights(float factor) { dirty_ = true; } +void TerrainEditor::mirrorX() { + if (!terrain_) return; + for (int cy = 0; cy < 16; cy++) { + for (int cx = 0; cx < 8; cx++) { + int srcIdx = cy * 16 + cx; + int dstIdx = cy * 16 + (15 - cx); + auto& src = terrain_->chunks[srcIdx]; + auto& dst = terrain_->chunks[dstIdx]; + if (!src.hasHeightMap() || !dst.hasHeightMap()) continue; + for (int v = 0; v < 145; v++) { + int row = v / 17, col = v % 17; + if (col > 8) continue; + int mirrorCol = 8 - col; + int mirrorV = row * 17 + mirrorCol; + dst.heightMap.heights[mirrorV] = src.heightMap.heights[v]; + } + dirtyChunks_.push_back(dstIdx); + } + } + for (int ci = 0; ci < 256; ci++) stitchEdges(ci); + dirty_ = true; +} + +void TerrainEditor::mirrorY() { + if (!terrain_) return; + for (int cy = 0; cy < 8; cy++) { + for (int cx = 0; cx < 16; cx++) { + int srcIdx = cy * 16 + cx; + int dstIdx = (15 - cy) * 16 + cx; + auto& src = terrain_->chunks[srcIdx]; + auto& dst = terrain_->chunks[dstIdx]; + if (!src.hasHeightMap() || !dst.hasHeightMap()) continue; + for (int v = 0; v < 145; v++) { + int row = v / 17, col = v % 17; + if (col > 8) continue; + int mirrorRow = 8 - row; + int mirrorV = mirrorRow * 17 + col; + dst.heightMap.heights[mirrorV] = src.heightMap.heights[v]; + } + dirtyChunks_.push_back(dstIdx); + } + } + for (int ci = 0; ci < 256; ci++) stitchEdges(ci); + dirty_ = true; +} + void TerrainEditor::copyStamp(const glm::vec3& center, float radius) { if (!terrain_) return; stampData_.clear(); diff --git a/tools/editor/terrain_editor.hpp b/tools/editor/terrain_editor.hpp index 4450a6e6..6dae6a6f 100644 --- a/tools/editor/terrain_editor.hpp +++ b/tools/editor/terrain_editor.hpp @@ -68,6 +68,10 @@ public: void pasteStamp(const glm::vec3& center); bool hasStamp() const { return !stampData_.empty(); } + // Mirror terrain along X or Y axis through tile center + void mirrorX(); + void mirrorY(); + // Import/export heightmap (raw 16-bit grayscale, 129x129) bool importHeightmap(const std::string& path, float heightScale); bool exportHeightmap(const std::string& path, float heightScale);