feat(editor): visualize patrol path of selected NPC as ribbon with waypoints

Adds setPatrolPath() that draws a multi-segment orange ribbon between the
NPC's spawn position and each waypoint, plus diamond markers at each point
(green for start = NPC home, white for waypoints). Renders only while the
NPC is selected and has a patrol path defined.
This commit is contained in:
Kelsi 2026-05-06 00:58:30 -07:00
parent 191ff9ec16
commit eb8f5a09b1
3 changed files with 109 additions and 0 deletions

View file

@ -133,6 +133,18 @@ void EditorApp::run() {
gizmo.setMode(TransformMode::None);
}
// Patrol path visualization for the selected NPC
if (auto* selNpc = npcSpawner_.getSelected();
selNpc && !selNpc->patrolPath.empty()) {
std::vector<glm::vec3> pts;
pts.reserve(selNpc->patrolPath.size() + 1);
pts.push_back(selNpc->position);
for (const auto& wp : selNpc->patrolPath) pts.push_back(wp.position);
viewport_.setPatrolPath(pts);
} else {
viewport_.clearPatrolPath();
}
uint32_t imageIndex = 0;
VkCommandBuffer cmd = vkCtx->beginFrame(imageIndex);
if (cmd == VK_NULL_HANDLE) continue;

View file

@ -60,6 +60,7 @@ void EditorViewport::shutdown() {
if (npcMarkerVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), npcMarkerVB_, npcMarkerVBAlloc_); npcMarkerVB_ = VK_NULL_HANDLE; }
if (brushVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), brushVB_, brushVBAlloc_); brushVB_ = VK_NULL_HANDLE; }
if (pathVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), pathVB_, pathVBAlloc_); pathVB_ = VK_NULL_HANDLE; }
if (patrolVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), patrolVB_, patrolVBAlloc_); patrolVB_ = VK_NULL_HANDLE; }
gizmo_.shutdown();
waterRenderer_.shutdown();
@ -505,6 +506,80 @@ void EditorViewport::setPathPreview(const glm::vec3& start, const glm::vec3& end
}
}
void EditorViewport::setPatrolPath(const std::vector<glm::vec3>& points, float width) {
if (patrolVB_) {
vmaDestroyBuffer(vkCtx_->getAllocator(), patrolVB_, patrolVBAlloc_);
patrolVB_ = VK_NULL_HANDLE;
patrolVertCount_ = 0;
}
if (points.size() < 2) return;
struct BV { float pos[3]; float color[4]; };
std::vector<BV> verts;
verts.reserve(points.size() * 24);
auto addRibbon = [&](const glm::vec3& a, const glm::vec3& b, float r, float g, float bl, float al) {
glm::vec2 dir = glm::vec2(b.x - a.x, b.y - a.y);
float len = glm::length(dir);
if (len < 0.001f) return;
dir /= len;
glm::vec2 perp(-dir.y, dir.x);
float hw = width * 0.5f;
float z0 = a.z + 1.5f;
float z1 = b.z + 1.5f;
BV v;
v.color[0] = r; v.color[1] = g; v.color[2] = bl; v.color[3] = al;
v.pos[0] = a.x - perp.x*hw; v.pos[1] = a.y - perp.y*hw; v.pos[2] = z0; verts.push_back(v);
v.pos[0] = a.x + perp.x*hw; v.pos[1] = a.y + perp.y*hw; v.pos[2] = z0; verts.push_back(v);
v.pos[0] = b.x - perp.x*hw; v.pos[1] = b.y - perp.y*hw; v.pos[2] = z1; verts.push_back(v);
v.pos[0] = b.x - perp.x*hw; v.pos[1] = b.y - perp.y*hw; v.pos[2] = z1; verts.push_back(v);
v.pos[0] = a.x + perp.x*hw; v.pos[1] = a.y + perp.y*hw; v.pos[2] = z0; verts.push_back(v);
v.pos[0] = b.x + perp.x*hw; v.pos[1] = b.y + perp.y*hw; v.pos[2] = z1; verts.push_back(v);
};
auto addWaypoint = [&](const glm::vec3& p, float r, float g, float bl) {
float s = 1.5f;
BV v;
v.color[0] = r; v.color[1] = g; v.color[2] = bl; v.color[3] = 0.95f;
glm::vec3 top(p.x, p.y, p.z + s * 2);
glm::vec3 bot(p.x, p.y, p.z + 0.2f);
glm::vec3 n(p.x, p.y + s, p.z + s);
glm::vec3 s2(p.x, p.y - s, p.z + s);
glm::vec3 e(p.x + s, p.y, p.z + s);
glm::vec3 w(p.x - s, p.y, p.z + s);
auto pushV = [&](const glm::vec3& vv){ v.pos[0]=vv.x; v.pos[1]=vv.y; v.pos[2]=vv.z; verts.push_back(v); };
pushV(top); pushV(n); pushV(e);
pushV(top); pushV(e); pushV(s2);
pushV(top); pushV(s2); pushV(w);
pushV(top); pushV(w); pushV(n);
pushV(bot); pushV(e); pushV(n);
pushV(bot); pushV(s2); pushV(e);
pushV(bot); pushV(w); pushV(s2);
pushV(bot); pushV(n); pushV(w);
};
for (size_t i = 0; i + 1 < points.size(); i++) {
addRibbon(points[i], points[i+1], 1.0f, 0.7f, 0.2f, 0.55f);
}
for (size_t i = 0; i < points.size(); i++) {
bool isStart = (i == 0);
addWaypoint(points[i], isStart ? 0.2f : 1.0f, isStart ? 1.0f : 0.85f, isStart ? 0.3f : 0.2f);
}
patrolVertCount_ = static_cast<uint32_t>(verts.size());
VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bufInfo.size = verts.size() * sizeof(BV);
bufInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
VmaAllocationCreateInfo allocInfo{};
allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
VmaAllocationInfo mapInfo{};
if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufInfo, &allocInfo,
&patrolVB_, &patrolVBAlloc_, &mapInfo) == VK_SUCCESS) {
std::memcpy(mapInfo.pMappedData, verts.data(), verts.size() * sizeof(BV));
}
}
void EditorViewport::updateNpcMarkers(const std::vector<CreatureSpawn>& npcs) {
if (npcMarkerVB_) {
vmaDestroyBuffer(vkCtx_->getAllocator(), npcMarkerVB_, npcMarkerVBAlloc_);
@ -685,6 +760,20 @@ void EditorViewport::render(VkCommandBuffer cmd) {
}
}
// Patrol path ribbon for selected NPC
if (patrolVB_ && patrolVertCount_ > 0) {
auto* waterPipeline = waterRenderer_.getPipeline();
auto* waterLayout = waterRenderer_.getPipelineLayout();
if (waterPipeline && waterLayout) {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterLayout,
0, 1, &perFrameSet, 0, nullptr);
VkDeviceSize off = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &patrolVB_, &off);
vkCmdDraw(cmd, patrolVertCount_, 1, 0, 0);
}
}
gizmo_.render(cmd, perFrameSet);
// NPC markers — render with water pipeline (pos+color, alpha blend)

View file

@ -53,6 +53,9 @@ public:
TransformGizmo& getGizmo() { return gizmo_; }
void setBrushIndicator(const glm::vec3& center, float radius, bool active);
void setPathPreview(const glm::vec3& start, const glm::vec3& end, float width, bool visible);
/** Show a multi-segment patrol path as a ribbon. Empty `points` clears it. */
void setPatrolPath(const std::vector<glm::vec3>& points, float width = 1.5f);
void clearPatrolPath() { setPatrolPath({}); }
void setWireframe(bool enabled);
void setShowNpcMarkers(bool show) { showNpcMarkers_ = show; }
@ -134,6 +137,11 @@ private:
VmaAllocation pathVBAlloc_ = VK_NULL_HANDLE;
uint32_t pathVertCount_ = 0;
bool pathVisible_ = false;
// Patrol path ribbon (selected NPC)
VkBuffer patrolVB_ = VK_NULL_HANDLE;
VmaAllocation patrolVBAlloc_ = VK_NULL_HANDLE;
uint32_t patrolVertCount_ = 0;
};
} // namespace editor