feat(editor): river/road tool supports multi-point polylines
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run

Path capture replaced with std::vector<glm::vec3> pathPoints_. The
flow is now: click "Click Start Point" → click terrain for first
point → keep clicking for additional waypoints → press Apply when
done, or Cancel to scrap. Apply iterates each consecutive pair as a
segment, calling carveRiver + paintAlongPath + fillWaterAlongPath
(or flattenRoad for road mode) per segment.

Hard-capped at 64 captured points so a runaway click handler can't
unboundedly grow the polyline. Toast at end reports the segment
count so users see the polyline took.

PathCapture states extended: None / WaitingStart / WaitingEnd /
WaitingMore. Backwards-compatible getPathStart()/getPathEnd()
return first/last points so the existing path-preview wiring keeps
working.
This commit is contained in:
Kelsi 2026-05-07 10:32:19 -07:00
parent 158ab192f0
commit 4e4102bf4a
2 changed files with 68 additions and 39 deletions

View file

@ -152,14 +152,23 @@ void EditorUI::processActions(EditorApp& app) {
} }
void EditorUI::setPathPoint(const glm::vec3& pos) { void EditorUI::setPathPoint(const glm::vec3& pos) {
// Hard cap so a runaway click handler doesn't grow the polyline
// unboundedly. 64 segments is more than enough for a tile-sized
// river or road.
constexpr size_t kMaxPathPoints = 64;
if (pathPoints_.size() >= kMaxPathPoints) return;
if (pathCapture_ == PathCapture::WaitingStart) { if (pathCapture_ == PathCapture::WaitingStart) {
pathStart_ = pos; pathPoints_.clear();
pathStartSet_ = true; pathPoints_.push_back(pos);
pathCapture_ = PathCapture::WaitingEnd; pathCapture_ = PathCapture::WaitingEnd;
} else if (pathCapture_ == PathCapture::WaitingEnd) { } else if (pathCapture_ == PathCapture::WaitingEnd) {
pathEnd_ = pos; pathPoints_.push_back(pos);
pathEndSet_ = true; // After the second point we stay in capture mode but switch to
pathCapture_ = PathCapture::None; // WaitingMore so the user can either click "Apply" with the
// 2-segment polyline or keep adding waypoints.
pathCapture_ = PathCapture::WaitingMore;
} else if (pathCapture_ == PathCapture::WaitingMore) {
pathPoints_.push_back(pos);
} }
} }
@ -1023,11 +1032,9 @@ void EditorUI::renderBrushPanel(EditorApp& app) {
ImGui::SliderFloat("Width##path", &pathWidth_, 2.0f, 50.0f); ImGui::SliderFloat("Width##path", &pathWidth_, 2.0f, 50.0f);
if (pathMode_ == 0) ImGui::SliderFloat("Depth##path", &pathDepth_, 1.0f, 30.0f); if (pathMode_ == 0) ImGui::SliderFloat("Depth##path", &pathDepth_, 1.0f, 30.0f);
if (pathCapture_ == PathCapture::None && !pathStartSet_) { if (pathCapture_ == PathCapture::None && pathPoints_.empty()) {
if (ImGui::Button("Click Start Point", ImVec2(-1, 0))) { if (ImGui::Button("Click Start Point", ImVec2(-1, 0))) {
pathCapture_ = PathCapture::WaitingStart; pathCapture_ = PathCapture::WaitingStart;
pathStartSet_ = false;
pathEndSet_ = false;
app.showToast("Click terrain to set start point"); app.showToast("Click terrain to set start point");
} }
} else if (pathCapture_ == PathCapture::WaitingStart) { } else if (pathCapture_ == PathCapture::WaitingStart) {
@ -1036,39 +1043,54 @@ void EditorUI::renderBrushPanel(EditorApp& app) {
pathCapture_ = PathCapture::None; pathCapture_ = PathCapture::None;
} }
} else if (pathCapture_ == PathCapture::WaitingEnd) { } else if (pathCapture_ == PathCapture::WaitingEnd) {
ImGui::TextColored(ImVec4(0.3f, 1, 0.3f, 1), "Start set at (%.0f, %.0f) — click for END", ImGui::TextColored(ImVec4(0.3f, 1, 0.3f, 1),
pathStart_.x, pathStart_.y); "Start at (%.0f, %.0f) — click for next point",
pathPoints_[0].x, pathPoints_[0].y);
if (ImGui::SmallButton("Cancel##path")) { if (ImGui::SmallButton("Cancel##path")) {
clearPath(); clearPath();
} }
} else if (pathStartSet_ && pathEndSet_) { } else if (pathCapture_ == PathCapture::WaitingMore || isPathReady()) {
// Multi-point: show running count and let the user keep
// clicking to add waypoints, or hit Apply with the current
// polyline. The Apply branch iterates each segment.
ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1), ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1),
"Start: (%.0f,%.0f) End: (%.0f,%.0f)", pathStart_.x, pathStart_.y, pathEnd_.x, pathEnd_.y); "%zu point(s) captured — click for more, or Apply",
pathPoints_.size());
if (ImGui::Button("Apply Path", ImVec2(-1, 0))) { if (ImGui::Button("Apply Path", ImVec2(-1, 0))) {
if (pathMode_ == 0) { int segCount = 0;
app.getTerrainEditor().carveRiver(pathStart_, pathEnd_, pathWidth_, pathDepth_); for (size_t k = 0; k + 1 < pathPoints_.size(); ++k) {
app.getTexturePainter().paintAlongPath(pathStart_, pathEnd_, pathWidth_ * 1.5f, const glm::vec3& a = pathPoints_[k];
"Tileset\\Ashenvale\\AshenvaleSand.blp"); const glm::vec3& b = pathPoints_[k + 1];
// After carving, fill water in the chunks along if (pathMode_ == 0) {
// the river path so the channel actually looks app.getTerrainEditor().carveRiver(a, b, pathWidth_, pathDepth_);
// like a river. liquidType 0 = water (1=ocean, app.getTexturePainter().paintAlongPath(a, b,
// 2=magma, 3=slime). pathWidth_ * 1.5f,
app.getTerrainEditor().fillWaterAlongPath( "Tileset\\Ashenvale\\AshenvaleSand.blp");
pathStart_, pathEnd_, pathWidth_, 0); app.getTerrainEditor().fillWaterAlongPath(a, b,
app.showToast("River carved + banks textured + water filled"); pathWidth_, 0);
} else { } else {
app.getTerrainEditor().flattenRoad(pathStart_, pathEnd_, pathWidth_); app.getTerrainEditor().flattenRoad(a, b, pathWidth_);
app.getTexturePainter().paintAlongPath(pathStart_, pathEnd_, pathWidth_, app.getTexturePainter().paintAlongPath(a, b,
"Tileset\\Elwynn\\ElwynnCobblestoneBase.blp"); pathWidth_,
app.showToast("Road flattened + textured"); "Tileset\\Elwynn\\ElwynnCobblestoneBase.blp");
}
segCount++;
} }
if (pathMode_ == 0)
app.showToast("River applied across " +
std::to_string(segCount) + " segment(s)");
else
app.showToast("Road applied across " +
std::to_string(segCount) + " segment(s)");
clearPath(); clearPath();
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("Reset##path")) clearPath(); if (ImGui::SmallButton("Reset##path")) clearPath();
} else if (pathStartSet_) { } else if (!pathPoints_.empty()) {
if (ImGui::Button("Click End Point", ImVec2(-1, 0))) // Captured at least one point but the user dismissed the
pathCapture_ = PathCapture::WaitingEnd; // capture mode without setting more — let them resume.
if (ImGui::Button("Click Next Point", ImVec2(-1, 0)))
pathCapture_ = PathCapture::WaitingMore;
} }
} }

View file

@ -24,15 +24,23 @@ public:
PaintMode getPaintMode() const { return paintMode_; } PaintMode getPaintMode() const { return paintMode_; }
// Path point capture: when active, next terrain click sets the point // Path point capture: when active, next terrain click appends a
enum class PathCapture { None, WaitingStart, WaitingEnd }; // point. WaitingStart for the first, WaitingMore for any subsequent
// (the user can keep clicking until they hit Apply or Finish).
enum class PathCapture { None, WaitingStart, WaitingEnd, WaitingMore };
PathCapture getPathCapture() const { return pathCapture_; } PathCapture getPathCapture() const { return pathCapture_; }
void setPathPoint(const glm::vec3& pos); void setPathPoint(const glm::vec3& pos);
glm::vec3 getPathStart() const { return pathStart_; } // Backwards-compatible getters: start = first point, end = last.
glm::vec3 getPathEnd() const { return pathEnd_; } glm::vec3 getPathStart() const {
bool isPathReady() const { return pathStartSet_ && pathEndSet_; } return pathPoints_.empty() ? glm::vec3(0) : pathPoints_.front();
}
glm::vec3 getPathEnd() const {
return pathPoints_.empty() ? glm::vec3(0) : pathPoints_.back();
}
const std::vector<glm::vec3>& getPathPoints() const { return pathPoints_; }
bool isPathReady() const { return pathPoints_.size() >= 2; }
float getPathWidth() const { return pathWidth_; } float getPathWidth() const { return pathWidth_; }
void clearPath() { pathStartSet_ = false; pathEndSet_ = false; pathCapture_ = PathCapture::None; } void clearPath() { pathPoints_.clear(); pathCapture_ = PathCapture::None; }
private: private:
void renderMenuBar(EditorApp& app); void renderMenuBar(EditorApp& app);
@ -86,8 +94,7 @@ private:
// Path point capture // Path point capture
PathCapture pathCapture_ = PathCapture::None; PathCapture pathCapture_ = PathCapture::None;
glm::vec3 pathStart_{0}, pathEnd_{0}; std::vector<glm::vec3> pathPoints_;
bool pathStartSet_ = false, pathEndSet_ = false;
int pathMode_ = 0; // 0=river, 1=road int pathMode_ = 0; // 0=river, 1=road
float pathWidth_ = 8.0f, pathDepth_ = 5.0f; float pathWidth_ = 8.0f, pathDepth_ = 5.0f;
}; };