From 1ba1a50112e14c82142d383650587929111eb818 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 06:15:33 -0700 Subject: [PATCH] feat(editor): terrain stamp/clone tool for replicating terrain features - Copy Stamp: captures all vertex heights within brush radius at cursor position, storing relative offsets from center - Paste Stamp: applies the copied height pattern at a new location, finding nearest vertices and setting their heights - Stamp status shown in panel ("Stamp ready" / "No stamp copied") - Auto-stitches chunk edges after paste for seamless results - Useful for replicating hills, craters, or other terrain features --- tools/editor/editor_ui.cpp | 15 +++++++++ tools/editor/terrain_editor.cpp | 57 +++++++++++++++++++++++++++++++++ tools/editor/terrain_editor.hpp | 9 ++++++ 3 files changed, 81 insertions(+) diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index aa80d4de..50d896f6 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -452,6 +452,21 @@ void EditorUI::renderBrushPanel(EditorApp& app) { "Exaggerate (>1) or flatten (<1) terrain relief"); } + ImGui::Separator(); + if (ImGui::CollapsingHeader("Stamp / Clone")) { + auto& brush2 = app.getTerrainEditor().brush(); + if (ImGui::Button("Copy Stamp", ImVec2(120, 0)) && brush2.isActive()) + app.getTerrainEditor().copyStamp(brush2.getPosition(), s.radius); + ImGui::SameLine(); + if (ImGui::Button("Paste Stamp", ImVec2(120, 0)) && brush2.isActive() && + app.getTerrainEditor().hasStamp()) + app.getTerrainEditor().pasteStamp(brush2.getPosition()); + if (app.getTerrainEditor().hasStamp()) + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1), "Stamp ready"); + else + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1), "No stamp copied"); + } + ImGui::Separator(); ImGui::Text("Terrain Holes (cave entrances):"); auto& brush = app.getTerrainEditor().brush(); diff --git a/tools/editor/terrain_editor.cpp b/tools/editor/terrain_editor.cpp index c7e4c374..66f6ae57 100644 --- a/tools/editor/terrain_editor.cpp +++ b/tools/editor/terrain_editor.cpp @@ -666,6 +666,63 @@ void TerrainEditor::scaleHeights(float factor) { dirty_ = true; } +void TerrainEditor::copyStamp(const glm::vec3& center, float radius) { + if (!terrain_) return; + stampData_.clear(); + stampCenter_ = center; + + for (int ci = 0; ci < 256; ci++) { + if (!terrain_->chunks[ci].hasHeightMap()) continue; + for (int v = 0; v < 145; v++) { + glm::vec3 pos = chunkVertexWorldPos(ci, v); + float dx = pos.x - center.x; + float dy = pos.y - center.y; + if (std::sqrt(dx * dx + dy * dy) <= radius) { + StampVertex sv; + sv.dx = dx; + sv.dy = dy; + sv.height = terrain_->chunks[ci].heightMap.heights[v]; + stampData_.push_back(sv); + } + } + } + LOG_INFO("Stamp copied: ", stampData_.size(), " vertices in radius ", radius); +} + +void TerrainEditor::pasteStamp(const glm::vec3& center) { + if (!terrain_ || stampData_.empty()) return; + + for (const auto& sv : stampData_) { + float wx = center.x + sv.dx; + float wy = center.y + sv.dy; + + // Find nearest vertex and set its height + float bestDist = 1e30f; + int bestChunk = -1, bestVert = -1; + for (int ci = 0; ci < 256; ci++) { + if (!terrain_->chunks[ci].hasHeightMap()) continue; + for (int v = 0; v < 145; v++) { + glm::vec3 pos = chunkVertexWorldPos(ci, v); + float d = std::sqrt((pos.x - wx) * (pos.x - wx) + (pos.y - wy) * (pos.y - wy)); + if (d < bestDist && d < 3.0f) { + bestDist = d; + bestChunk = ci; + bestVert = v; + } + } + } + if (bestChunk >= 0) { + terrain_->chunks[bestChunk].heightMap.heights[bestVert] = sv.height; + if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), bestChunk) == dirtyChunks_.end()) + dirtyChunks_.push_back(bestChunk); + } + } + + for (int ci : dirtyChunks_) stitchEdges(ci); + dirty_ = true; + LOG_INFO("Stamp pasted at (", center.x, ",", center.y, ")"); +} + void TerrainEditor::clampHeights(float minH, float maxH) { if (!terrain_) return; for (int ci = 0; ci < 256; ci++) { diff --git a/tools/editor/terrain_editor.hpp b/tools/editor/terrain_editor.hpp index 8bb1d5a5..4450a6e6 100644 --- a/tools/editor/terrain_editor.hpp +++ b/tools/editor/terrain_editor.hpp @@ -63,6 +63,11 @@ public: // Scale all heights by a factor (useful for exaggerating or flattening) void scaleHeights(float factor); + // Terrain stamp: copy heights from source area, paste at destination + void copyStamp(const glm::vec3& center, float radius); + void pasteStamp(const glm::vec3& center); + bool hasStamp() const { return !stampData_.empty(); } + // Import/export heightmap (raw 16-bit grayscale, 129x129) bool importHeightmap(const std::string& path, float heightScale); bool exportHeightmap(const std::string& path, float heightScale); @@ -90,6 +95,10 @@ private: float getVertexHeight(int chunkIdx, int vertIdx) const; void setVertexHeight(int chunkIdx, int vertIdx, float height); + struct StampVertex { float dx, dy, height; }; + std::vector stampData_; + glm::vec3 stampCenter_{0}; + pipeline::ADTTerrain* terrain_ = nullptr; EditorBrush brush_; EditorHistory history_;