diff --git a/python_bindings/pyproject.toml b/python_bindings/pyproject.toml index 8c42c159..0c39603a 100644 --- a/python_bindings/pyproject.toml +++ b/python_bindings/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "slamd" -version = "2.1.9" +version = "2.1.10" description = "Python bindings for SlamDunk" authors = [{ name = "Robert Leo", email = "robert.leo.jonsson@gmail.com" }] readme = "README.md" diff --git a/slamd/include/slamd_window/tree/node.hpp b/slamd/include/slamd_window/tree/node.hpp index 9a1bae45..29ceb82c 100644 --- a/slamd/include/slamd_window/tree/node.hpp +++ b/slamd/include/slamd_window/tree/node.hpp @@ -28,6 +28,9 @@ class Node { mutable std::mutex object_mutex; public: + bool checked = true; + std::optional glob_matches = std::nullopt; + std::optional> get_object() const; std::optional get_transform() const; diff --git a/slamd/include/slamd_window/tree/tree.hpp b/slamd/include/slamd_window/tree/tree.hpp index 6e515ac2..f2d0f1f2 100644 --- a/slamd/include/slamd_window/tree/tree.hpp +++ b/slamd/include/slamd_window/tree/tree.hpp @@ -11,11 +11,11 @@ namespace slamd { class Tree { private: uint64_t id; - std::unique_ptr root; public: Tree(uint64_t id); Tree(uint64_t id, std::unique_ptr&& root); + std::unique_ptr root; virtual void set_object(const TreePath& path, std::shared_ptr<_geom::Geometry> object); @@ -31,6 +31,7 @@ class Tree { std::optional bounds(); void set_transform(const TreePath& path, const glm::mat4& transform); void clear(const TreePath& path); + void mark_nodes_matching_glob(std::optional glob); protected: std::optional traverse(const TreePath& path); diff --git a/slamd/include/slamd_window/tree/tree_path.hpp b/slamd/include/slamd_window/tree/tree_path.hpp index 5c707678..005da047 100644 --- a/slamd/include/slamd_window/tree/tree_path.hpp +++ b/slamd/include/slamd_window/tree/tree_path.hpp @@ -13,6 +13,7 @@ class TreePath { TreePath parent() const; std::string string() const; TreePath& operator=(const TreePath&) = default; + bool matches_glob(const TreePath& glob_path); public: std::vector components; diff --git a/slamd/include/slamd_window/view/canvas_view.hpp b/slamd/include/slamd_window/view/canvas_view.hpp index 10d80459..cc83462d 100644 --- a/slamd/include/slamd_window/view/canvas_view.hpp +++ b/slamd/include/slamd_window/view/canvas_view.hpp @@ -9,9 +9,6 @@ namespace slamd { class CanvasView : public View { - public: - std::shared_ptr tree; - private: FrameBuffer frame_buffer; Camera2D camera; diff --git a/slamd/include/slamd_window/view/scene_view.hpp b/slamd/include/slamd_window/view/scene_view.hpp index c39b27de..6031e8a9 100644 --- a/slamd/include/slamd_window/view/scene_view.hpp +++ b/slamd/include/slamd_window/view/scene_view.hpp @@ -10,9 +10,6 @@ namespace slamd { class SceneView : public View { - public: - std::shared_ptr tree; - private: FrameBuffer frame_buffer; Arcball arcball; diff --git a/slamd/include/slamd_window/view/view.hpp b/slamd/include/slamd_window/view/view.hpp index 117f69fb..12ecf207 100644 --- a/slamd/include/slamd_window/view/view.hpp +++ b/slamd/include/slamd_window/view/view.hpp @@ -6,10 +6,15 @@ namespace slamd { class View { public: + View(std::shared_ptr t); virtual void render_to_imgui() = 0; // pure virtual = abstract base class virtual ~View() = default; // virtual dtor for safe polymorphic deletion static std::unique_ptr deserialize(const flatb::View* view, std::shared_ptr tree); + std::shared_ptr tree; + + std::optional visualize_glob = std::nullopt; + char filter_buf[512] = ""; }; } // namespace slamd \ No newline at end of file diff --git a/slamd/src/window/run_window.cpp b/slamd/src/window/run_window.cpp index de2a9fa3..c26c1745 100644 --- a/slamd/src/window/run_window.cpp +++ b/slamd/src/window/run_window.cpp @@ -18,6 +18,227 @@ void framebuffer_size_callback( gl::glViewport(0, 0, width, height); } +inline void tree_menu( + View* view +) { + Node* root = view->tree->root.get(); + + // Compute a field width that fully shows the text (plus padding) + const float min_field_w = 250.0f; + const float text_w = ImGui::CalcTextSize(view->filter_buf).x; + const float pad_x = ImGui::GetStyle().FramePadding.x; + + // A little extra so the caret isn’t jammed at the edge + const float desired_field_w = text_w + 2.0f * pad_x + 12.0f; + const float field_w = + (desired_field_w < min_field_w) ? min_field_w : desired_field_w; + + std::optional filter_error_text; + std::optional filter_path; + std::string filter_string(view->filter_buf); + + if (filter_string.size() > 0) { + try { + filter_path = TreePath(filter_string); + } catch (const std::invalid_argument& e) { + filter_error_text = e.what(); + } + } + + view->tree->mark_nodes_matching_glob(filter_path); + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Filter:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(field_w); // take all remaining width on the line + + if (filter_error_text.has_value()) { + ImGui::PushStyleColor( + ImGuiCol_FrameBg, + ImVec4(0.35f, 0.10f, 0.10f, 1.0f) + ); + ImGui::PushStyleColor( + ImGuiCol_FrameBgHovered, + ImVec4(0.45f, 0.12f, 0.12f, 1.0f) + ); + ImGui::PushStyleColor( + ImGuiCol_FrameBgActive, + ImVec4(0.50f, 0.13f, 0.13f, 1.0f) + ); + ImGui::PushStyleColor( + ImGuiCol_Border, + ImVec4(0.90f, 0.20f, 0.20f, 1.0f) + ); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + } + ImGui::InputText( + "##filter", + view->filter_buf, + IM_ARRAYSIZE(view->filter_buf) + ); + if (filter_error_text.has_value()) { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + + // Tooltip on hover + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", filter_error_text.value().c_str()); + } + + // Inline red "!" marker + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.95f, 0.25f, 0.25f, 1.0f), "!"); + } + + ImGui::Separator(); + + std::function draw_node = + [&](Node* n, + std::string label, + int depth, + bool parent_dimmed, + TreePath& full_path) { + ImGui::PushID(n); + + if (filter_path.has_value()) { + n->glob_matches = full_path.matches_glob(filter_path.value()); + } else { + n->glob_matches = std::nullopt; + } + + const bool has_children = !n->children.empty(); + const ImGuiTreeNodeFlags base_flags = + ImGuiTreeNodeFlags_SpanAvailWidth | + ImGuiTreeNodeFlags_SpanFullWidth | + ImGuiTreeNodeFlags_FramePadding | + (has_children ? 0 + : ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_NoTreePushOnOpen); + + const bool dimmed = parent_dimmed || !n->checked; + bool dimmed_here = !parent_dimmed && !n->checked; + if (dimmed_here) { + float base_alpha = ImGui::GetStyle().Alpha; + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, base_alpha * 0.45f); + } + + // Checkbox aligned to the right of the same line + ImGui::Checkbox("##visible", &n->checked); + ImGui::SameLine(0.0f, 4.0f); + + bool open = false; + if (has_children) { + open = ImGui::TreeNodeEx(label.c_str(), base_flags); + } else { + ImGui::TreeNodeEx(label.c_str(), base_flags); + } + + if (n->glob_matches.has_value()) { + const bool match = n->glob_matches.value_or(false); + + // Get row rect from the last item (the tree node we just drew) + ImVec2 item_min = ImGui::GetItemRectMin(); + ImVec2 item_max = ImGui::GetItemRectMax(); + + // Compute right edge of the content area (screen coords) + ImVec2 win_pos = ImGui::GetWindowPos(); + ImVec2 cr_max = ImGui::GetWindowContentRegionMax(); + float right_edge_x = + win_pos.x + cr_max.x - ImGui::GetStyle().ItemInnerSpacing.x; + + // Circle params + float radius = 4.0f; + float cx = right_edge_x - radius; // right-aligned + float cy = + 0.5f * (item_min.y + item_max.y); // vertically centered + + ImU32 col_fill = + ImGui::GetColorU32(ImVec4(0.30f, 0.85f, 0.40f, 1.0f) + ); // green + ImU32 col_line = ImGui::GetColorU32( + ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled) + ); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + if (match) { + dl->AddCircleFilled(ImVec2(cx, cy), radius, col_fill); + } else { + dl->AddCircle(ImVec2(cx, cy), radius, col_line, 12, 1.5f); + } + } + + if (open) { + for (const auto& c : n->children) { + full_path.components.push_back(c.first); + draw_node( + c.second.get(), + c.first, + depth + 1, + dimmed, + full_path + ); + full_path.components.pop_back(); + } + ImGui::TreePop(); + } + if (dimmed_here) { + ImGui::PopStyleVar(); + } + ImGui::PopID(); + }; + + TreePath pth("/"); + draw_node(root, "/", 0, false, pth); +} + +inline void draw_tree_overlay( + View* view, + const char* overlay_id = "##scene_tree_overlay", + const char* header = "Tree", + float margin = 8.0f, + float min_width = 100.0f +) { + // Anchor to current window's content region (screen coords) + ImVec2 win_pos = ImGui::GetWindowPos(); + ImVec2 cr_min = ImGui::GetWindowContentRegionMin(); + ImVec2 cr_max = ImGui::GetWindowContentRegionMax(); + ImVec2 tl(win_pos.x + cr_min.x, win_pos.y + cr_min.y); + ImVec2 br(win_pos.x + cr_max.x, win_pos.y + cr_max.y); + + ImVec2 pos(tl.x + margin, tl.y + margin); + ImVec2 max_size(br.x - tl.x - 2 * margin, br.y - tl.y - 2 * margin); + + // Style: dark translucent bg, rounded corners, slim padding + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6, 6)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0.55f)); + + ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(0, 0)); + ImGui::SetNextWindowViewport(ImGui::GetWindowViewport()->ID); + ImGui::SetNextWindowSizeConstraints(ImVec2(min_width, 0), max_size); + + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove; // pinned + + ImGui::Begin(overlay_id, nullptr, flags); + + if (header && *header) { + ImGui::TextUnformatted(header); + ImGui::Separator(); + } + + // Reuse your tree renderer (defaults collapsed) + tree_menu(view); + + ImGui::End(); + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(3); +} + void run_window( StateManager& state_manager ) { @@ -90,6 +311,9 @@ void run_window( ImGuiWindowFlags_NoScrollWithMouse ); scene->render_to_imgui(); + + draw_tree_overlay(scene.get()); + ImGui::End(); ImGui::PopStyleVar(); } diff --git a/slamd/src/window/tree/tree.cpp b/slamd/src/window/tree/tree.cpp index 3f2af726..8be99492 100644 --- a/slamd/src/window/tree/tree.cpp +++ b/slamd/src/window/tree/tree.cpp @@ -18,6 +18,31 @@ Tree::Tree( : id(id), root(std::move(root)) {} +void mark_glob_match_recursive( + Node* node, + TreePath& path, + std::optional& glob +) { + if (!glob.has_value()) { + node->glob_matches = std::nullopt; + } else { + node->glob_matches = path.matches_glob(glob.value()); + } + + for (auto& [label, child] : node->children) { + path.components.push_back(label); + mark_glob_match_recursive(child.get(), path, glob); + path.components.pop_back(); + } +} + +void Tree::mark_nodes_matching_glob( + std::optional glob +) { + TreePath pth("/"); + mark_glob_match_recursive(this->root.get(), pth, glob); +} + std::shared_ptr Tree::deserialize( const slamd::flatb::Tree* serialized, std::map>& @@ -119,6 +144,10 @@ void Tree::render_recursive( const glm::mat4& view, const glm::mat4& projection ) const { + if (!node->glob_matches.has_value() && !node->checked) { + return; + } + glm::mat4 next_transform = current_transform; auto node_transform = node->get_transform(); @@ -128,7 +157,7 @@ void Tree::render_recursive( const auto node_object = node->get_object(); - if (node_object.has_value()) { + if (node_object.has_value() && node->glob_matches.value_or(true)) { node_object.value()->render(next_transform, view, projection); } diff --git a/slamd/src/window/tree/tree_path.cpp b/slamd/src/window/tree/tree_path.cpp index d62a1f34..87830e9d 100644 --- a/slamd/src/window/tree/tree_path.cpp +++ b/slamd/src/window/tree/tree_path.cpp @@ -93,4 +93,66 @@ TreePath operator/( return TreePath(new_components); } +bool match_component( + const std::string& pattern, + const std::string& text +) { + // only * is supported (not ? or []) + size_t pi = 0, ti = 0, star = std::string::npos, match = 0; + while (ti < text.size()) { + if (pi < pattern.size() && + (pattern[pi] == text[ti] || pattern[pi] == '?')) { + ++pi; + ++ti; + } else if (pi < pattern.size() && pattern[pi] == '*') { + star = pi++; + match = ti; + } else if (star != std::string::npos) { + pi = star + 1; + ti = ++match; + } else { + return false; + } + } + while (pi < pattern.size() && pattern[pi] == '*') { + ++pi; + } + return pi == pattern.size(); +} + +bool match_glob( + const std::vector& path, + const std::vector& pattern, + size_t pi = 0, + size_t si = 0 +) { + while (pi < pattern.size()) { + if (pattern[pi] == "**") { + // try to consume any number of segments + for (size_t skip = 0; si + skip <= path.size(); ++skip) { + if (match_glob(path, pattern, pi + 1, si + skip)) { + return true; + } + } + return false; + } else { + if (si >= path.size()) { + return false; + } + if (!match_component(pattern[pi], path[si])) { + return false; + } + ++pi; + ++si; + } + } + return si == path.size(); +} + +bool TreePath::matches_glob( + const TreePath& glob_path +) { + return match_glob(this->components, glob_path.components); +} + } // namespace slamd \ No newline at end of file diff --git a/slamd/src/window/view/canvas_view.cpp b/slamd/src/window/view/canvas_view.cpp index d51f8626..6f421989 100644 --- a/slamd/src/window/view/canvas_view.cpp +++ b/slamd/src/window/view/canvas_view.cpp @@ -9,7 +9,7 @@ namespace slamd { CanvasView::CanvasView( std::shared_ptr tree ) - : tree(tree), + : View(std::move(tree)), frame_buffer(500, 500), camera(slamd::gmath::Rect2D({0.0, 0.0}, {1.0, 1.0})), manually_moved(false) {} diff --git a/slamd/src/window/view/scene_view.cpp b/slamd/src/window/view/scene_view.cpp index 9cf1a561..5e344813 100644 --- a/slamd/src/window/view/scene_view.cpp +++ b/slamd/src/window/view/scene_view.cpp @@ -16,7 +16,7 @@ glm::vec3 make_background_color( SceneView::SceneView( std::shared_ptr tree ) - : tree(tree), + : View(std::move(tree)), frame_buffer(500, 500), camera(45.0, 0.1f, 100000.0f), xy_grid(1000.0) { diff --git a/slamd/src/window/view/view.cpp b/slamd/src/window/view/view.cpp index 752ecee2..1a22d35b 100644 --- a/slamd/src/window/view/view.cpp +++ b/slamd/src/window/view/view.cpp @@ -4,6 +4,11 @@ namespace slamd { +View::View( + std::shared_ptr t +) + : tree(std::move(t)) {} + std::unique_ptr View::deserialize( const flatb::View* view_fb, std::shared_ptr tree