From 0bdf35a461084d00ffe036fcbd3d2aa4406bb6cd Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 8 Apr 2026 00:10:18 +0200 Subject: [PATCH] feat: add C++ ImGui functions for core UI and visualization Funciones C++/ImGui para dashboards (grid, panel, docking, sidebar, tabs), visualizaciones (candlestick, gauge, histogram, pie, sparkline, heatmap, scatter, line, bar, surface3d, kpi, table), grafos (force layout, renderer, viewport, spatial hash, types) y utilidades (time series buffer, tracy zones, memory/fps overlay, plot theme). Co-Authored-By: Claude Opus 4.6 (1M context) --- cpp/functions/core/dashboard_grid.cpp | 60 +++ cpp/functions/core/dashboard_grid.h | 17 + cpp/functions/core/dashboard_grid.md | 88 +++++ cpp/functions/core/dashboard_panel.cpp | 24 ++ cpp/functions/core/dashboard_panel.h | 14 + cpp/functions/core/dashboard_panel.md | 57 +++ cpp/functions/core/docking_layout.cpp | 53 +++ cpp/functions/core/docking_layout.h | 16 + cpp/functions/core/docking_layout.md | 64 ++++ cpp/functions/core/graph_spatial_hash.cpp | 174 +++++++++ cpp/functions/core/graph_spatial_hash.h | 39 ++ cpp/functions/core/graph_spatial_hash.md | 71 ++++ cpp/functions/core/memory_overlay.cpp | 99 +++++ cpp/functions/core/memory_overlay.h | 7 + cpp/functions/core/memory_overlay.md | 56 +++ cpp/functions/core/plot_theme.cpp | 131 +++++++ cpp/functions/core/plot_theme.h | 26 ++ cpp/functions/core/plot_theme.md | 96 +++++ cpp/functions/core/sidebar.cpp | 25 ++ cpp/functions/core/sidebar.h | 14 + cpp/functions/core/sidebar.md | 59 +++ cpp/functions/core/tab_container.cpp | 21 + cpp/functions/core/tab_container.h | 20 + cpp/functions/core/tab_container.md | 64 ++++ cpp/functions/core/time_series_buffer.cpp | 90 +++++ cpp/functions/core/time_series_buffer.h | 31 ++ cpp/functions/core/time_series_buffer.md | 59 +++ cpp/functions/core/tracy_zone.cpp | 5 + cpp/functions/core/tracy_zone.h | 36 ++ cpp/functions/core/tracy_zone.md | 83 ++++ cpp/functions/viz/candlestick.cpp | 81 ++++ cpp/functions/viz/candlestick.h | 9 + cpp/functions/viz/candlestick.md | 67 ++++ cpp/functions/viz/gauge.cpp | 94 +++++ cpp/functions/viz/gauge.h | 6 + cpp/functions/viz/gauge.md | 59 +++ cpp/functions/viz/graph_force_layout.cpp | 353 +++++++++++++++++ cpp/functions/viz/graph_force_layout.h | 27 ++ cpp/functions/viz/graph_force_layout.md | 79 ++++ cpp/functions/viz/graph_renderer.cpp | 446 ++++++++++++++++++++++ cpp/functions/viz/graph_renderer.h | 28 ++ cpp/functions/viz/graph_renderer.md | 87 +++++ cpp/functions/viz/graph_types.cpp | 24 ++ cpp/functions/viz/graph_types.h | 50 +++ cpp/functions/viz/graph_viewport.cpp | 327 ++++++++++++++++ cpp/functions/viz/graph_viewport.h | 50 +++ cpp/functions/viz/graph_viewport.md | 119 ++++++ cpp/functions/viz/histogram.cpp | 18 + cpp/functions/viz/histogram.h | 7 + cpp/functions/viz/histogram.md | 42 ++ cpp/functions/viz/kpi_card.cpp | 44 +++ cpp/functions/viz/kpi_card.h | 16 + cpp/functions/viz/kpi_card.md | 71 ++++ cpp/functions/viz/pie_chart.cpp | 31 ++ cpp/functions/viz/pie_chart.h | 7 + cpp/functions/viz/pie_chart.md | 46 +++ cpp/functions/viz/sparkline.cpp | 76 ++++ cpp/functions/viz/sparkline.h | 12 + cpp/functions/viz/sparkline.md | 69 ++++ cpp/functions/viz/surface_plot_3d.cpp | 12 + cpp/functions/viz/surface_plot_3d.h | 8 + cpp/functions/viz/surface_plot_3d.md | 61 +++ cpp/functions/viz/table_view.cpp | 32 ++ cpp/functions/viz/table_view.h | 6 + cpp/functions/viz/table_view.md | 67 ++++ cpp/types/viz/graph_types.md | 106 +++++ 66 files changed, 4236 insertions(+) create mode 100644 cpp/functions/core/dashboard_grid.cpp create mode 100644 cpp/functions/core/dashboard_grid.h create mode 100644 cpp/functions/core/dashboard_grid.md create mode 100644 cpp/functions/core/dashboard_panel.cpp create mode 100644 cpp/functions/core/dashboard_panel.h create mode 100644 cpp/functions/core/dashboard_panel.md create mode 100644 cpp/functions/core/docking_layout.cpp create mode 100644 cpp/functions/core/docking_layout.h create mode 100644 cpp/functions/core/docking_layout.md create mode 100644 cpp/functions/core/graph_spatial_hash.cpp create mode 100644 cpp/functions/core/graph_spatial_hash.h create mode 100644 cpp/functions/core/graph_spatial_hash.md create mode 100644 cpp/functions/core/memory_overlay.cpp create mode 100644 cpp/functions/core/memory_overlay.h create mode 100644 cpp/functions/core/memory_overlay.md create mode 100644 cpp/functions/core/plot_theme.cpp create mode 100644 cpp/functions/core/plot_theme.h create mode 100644 cpp/functions/core/plot_theme.md create mode 100644 cpp/functions/core/sidebar.cpp create mode 100644 cpp/functions/core/sidebar.h create mode 100644 cpp/functions/core/sidebar.md create mode 100644 cpp/functions/core/tab_container.cpp create mode 100644 cpp/functions/core/tab_container.h create mode 100644 cpp/functions/core/tab_container.md create mode 100644 cpp/functions/core/time_series_buffer.cpp create mode 100644 cpp/functions/core/time_series_buffer.h create mode 100644 cpp/functions/core/time_series_buffer.md create mode 100644 cpp/functions/core/tracy_zone.cpp create mode 100644 cpp/functions/core/tracy_zone.h create mode 100644 cpp/functions/core/tracy_zone.md create mode 100644 cpp/functions/viz/candlestick.cpp create mode 100644 cpp/functions/viz/candlestick.h create mode 100644 cpp/functions/viz/candlestick.md create mode 100644 cpp/functions/viz/gauge.cpp create mode 100644 cpp/functions/viz/gauge.h create mode 100644 cpp/functions/viz/gauge.md create mode 100644 cpp/functions/viz/graph_force_layout.cpp create mode 100644 cpp/functions/viz/graph_force_layout.h create mode 100644 cpp/functions/viz/graph_force_layout.md create mode 100644 cpp/functions/viz/graph_renderer.cpp create mode 100644 cpp/functions/viz/graph_renderer.h create mode 100644 cpp/functions/viz/graph_renderer.md create mode 100644 cpp/functions/viz/graph_types.cpp create mode 100644 cpp/functions/viz/graph_types.h create mode 100644 cpp/functions/viz/graph_viewport.cpp create mode 100644 cpp/functions/viz/graph_viewport.h create mode 100644 cpp/functions/viz/graph_viewport.md create mode 100644 cpp/functions/viz/histogram.cpp create mode 100644 cpp/functions/viz/histogram.h create mode 100644 cpp/functions/viz/histogram.md create mode 100644 cpp/functions/viz/kpi_card.cpp create mode 100644 cpp/functions/viz/kpi_card.h create mode 100644 cpp/functions/viz/kpi_card.md create mode 100644 cpp/functions/viz/pie_chart.cpp create mode 100644 cpp/functions/viz/pie_chart.h create mode 100644 cpp/functions/viz/pie_chart.md create mode 100644 cpp/functions/viz/sparkline.cpp create mode 100644 cpp/functions/viz/sparkline.h create mode 100644 cpp/functions/viz/sparkline.md create mode 100644 cpp/functions/viz/surface_plot_3d.cpp create mode 100644 cpp/functions/viz/surface_plot_3d.h create mode 100644 cpp/functions/viz/surface_plot_3d.md create mode 100644 cpp/functions/viz/table_view.cpp create mode 100644 cpp/functions/viz/table_view.h create mode 100644 cpp/functions/viz/table_view.md create mode 100644 cpp/types/viz/graph_types.md diff --git a/cpp/functions/core/dashboard_grid.cpp b/cpp/functions/core/dashboard_grid.cpp new file mode 100644 index 00000000..fe63f4c7 --- /dev/null +++ b/cpp/functions/core/dashboard_grid.cpp @@ -0,0 +1,60 @@ +#include "dashboard_grid.h" +#include +#include + +// Internal state stack to support nested grids. +namespace { + +struct GridState { + int columns; + float spacing; + float col_width; + int counter; // number of dashboard_grid_next() calls so far +}; + +static std::vector g_grid_stack; + +} // namespace + +void dashboard_grid_begin(int columns, float spacing) { + if (columns < 1) columns = 1; + + float available = ImGui::GetContentRegionAvail().x; + float col_width = (available - spacing * static_cast(columns - 1)) + / static_cast(columns); + if (col_width < 1.0f) col_width = 1.0f; + + g_grid_stack.push_back({columns, spacing, col_width, 0}); + + ImGui::BeginGroup(); + ImGui::PushItemWidth(col_width); +} + +void dashboard_grid_next() { + if (g_grid_stack.empty()) return; + + GridState& s = g_grid_stack.back(); + + ImGui::PopItemWidth(); + ImGui::EndGroup(); + + s.counter++; + + if (s.counter % s.columns != 0) { + // Same row: advance horizontally. + ImGui::SameLine(0.0f, s.spacing); + } + // If counter % columns == 0 the next BeginGroup starts a new row automatically. + + ImGui::BeginGroup(); + ImGui::PushItemWidth(s.col_width); +} + +void dashboard_grid_end() { + if (g_grid_stack.empty()) return; + + ImGui::PopItemWidth(); + ImGui::EndGroup(); + + g_grid_stack.pop_back(); +} diff --git a/cpp/functions/core/dashboard_grid.h b/cpp/functions/core/dashboard_grid.h new file mode 100644 index 00000000..fd30ba43 --- /dev/null +++ b/cpp/functions/core/dashboard_grid.h @@ -0,0 +1,17 @@ +#pragma once + +// Dashboard grid — distributes child widgets in N columns. +// Usage: +// dashboard_grid_begin(3); +// // widget 1 (auto placed in col 0) +// dashboard_grid_next(); +// // widget 2 (auto placed in col 1) +// dashboard_grid_next(); +// // widget 3 (auto placed in col 2, wraps to next row) +// dashboard_grid_next(); +// // widget 4 (col 0 of row 2) +// dashboard_grid_end(); + +void dashboard_grid_begin(int columns = 2, float spacing = 8.0f); +void dashboard_grid_next(); // advance to next cell +void dashboard_grid_end(); diff --git a/cpp/functions/core/dashboard_grid.md b/cpp/functions/core/dashboard_grid.md new file mode 100644 index 00000000..bae08919 --- /dev/null +++ b/cpp/functions/core/dashboard_grid.md @@ -0,0 +1,88 @@ +--- +name: dashboard_grid +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "void dashboard_grid_begin(int columns = 2, float spacing = 8.0f); void dashboard_grid_next(); void dashboard_grid_end()" +description: "Grid de N columnas para distribuir widgets de dashboard automaticamente con spacing uniforme entre columnas" +tags: [imgui, grid, layout, dashboard, responsive] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/dashboard_grid.cpp" +framework: imgui +params: + - name: columns + desc: "Numero de columnas del grid (minimo 1); el ancho disponible se divide uniformemente entre ellas" + - name: spacing + desc: "Espacio horizontal entre columnas en pixels" +output: "Layout de grid aplicado al contenido entre dashboard_grid_begin/end; cada celda recibe un ancho uniforme calculado a partir del espacio disponible" +--- + +# dashboard_grid + +Divide el ancho disponible en N columnas con spacing uniforme y posiciona cada widget en su celda automaticamente. Soporta grids anidados mediante un stack interno de estado. + +## Uso + +```cpp +dashboard_grid_begin(3, 8.0f); + +// Celda 0 +dashboard_panel_begin("CPU"); +ImGui::Text("%.1f %%", cpu_pct); +dashboard_panel_end(); +dashboard_grid_next(); + +// Celda 1 +dashboard_panel_begin("Memory"); +ImGui::Text("%.0f MB", mem_mb); +dashboard_panel_end(); +dashboard_grid_next(); + +// Celda 2 +dashboard_panel_begin("Disk"); +ImGui::Text("%.0f GB", disk_gb); +dashboard_panel_end(); + +dashboard_grid_end(); +``` + +## Implementacion + +### Calculo de ancho + +``` +col_width = (available_width - spacing * (columns - 1)) / columns +``` + +`available_width` se obtiene de `ImGui::GetContentRegionAvail().x` en el momento de `dashboard_grid_begin`. + +### Mecanica de celdas + +Cada celda es un `BeginGroup`/`EndGroup` con `PushItemWidth(col_width)` para que los widgets internos respeten el ancho de columna. Al llamar `dashboard_grid_next`: + +1. Se cierra la celda actual (`PopItemWidth`, `EndGroup`). +2. Se incrementa el contador interno. +3. Si `counter % columns != 0` se emite `SameLine(0, spacing)` para continuar en la misma fila. +4. Si `counter % columns == 0` no se emite `SameLine`: ImGui pasa a la siguiente fila automaticamente. +5. Se abre la nueva celda (`BeginGroup`, `PushItemWidth`). + +### Grids anidados + +El estado (columnas, spacing, col_width, counter) se guarda en `g_grid_stack` (un `std::vector` de structs en un namespace anonimo). Cada llamada a `dashboard_grid_begin` hace `push_back` y `dashboard_grid_end` hace `pop_back`, permitiendo anidar grids sin conflicto. + +## Notas + +- Llamar `dashboard_grid_next()` entre cada par de widgets, **no** antes del primero ni despues del ultimo. +- El numero de `dashboard_grid_next()` puede ser mayor que `columns - 1`: el grid hace wrap automatico a la siguiente fila. +- Combina bien con `dashboard_panel_begin`/`dashboard_panel_end` para crear dashboards con paneles alineados en cuadricula. +- Si `columns <= 0` se fuerza a 1 para evitar division por cero. diff --git a/cpp/functions/core/dashboard_panel.cpp b/cpp/functions/core/dashboard_panel.cpp new file mode 100644 index 00000000..272c7854 --- /dev/null +++ b/cpp/functions/core/dashboard_panel.cpp @@ -0,0 +1,24 @@ +#include "dashboard_panel.h" +#include + +bool dashboard_panel_begin(const char* title, float min_width, float min_height) { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.15f, 1.0f)); + + ImVec2 size(min_width > 0.0f ? min_width : 0.0f, + min_height > 0.0f ? min_height : 0.0f); + + ImGui::BeginChild(title, size, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + + ImGui::TextUnformatted(title); + ImGui::Separator(); + + return true; +} + +void dashboard_panel_end() { + ImGui::EndChild(); + ImGui::PopStyleColor(1); + ImGui::PopStyleVar(2); +} diff --git a/cpp/functions/core/dashboard_panel.h b/cpp/functions/core/dashboard_panel.h new file mode 100644 index 00000000..fbb7ce68 --- /dev/null +++ b/cpp/functions/core/dashboard_panel.h @@ -0,0 +1,14 @@ +#pragma once + +// Dashboard panel — a styled child window with title bar. +// Usage: +// if (dashboard_panel_begin("Sales")) { +// line_plot("Revenue", xs, ys, N); +// } +// dashboard_panel_end(); // ALWAYS call, even if begin returned false +// +// Features: title bar with text, rounded corners, subtle border, auto-resize. +// min_width/min_height set minimum size constraints. + +bool dashboard_panel_begin(const char* title, float min_width = 200.0f, float min_height = 150.0f); +void dashboard_panel_end(); diff --git a/cpp/functions/core/dashboard_panel.md b/cpp/functions/core/dashboard_panel.md new file mode 100644 index 00000000..1e3d4373 --- /dev/null +++ b/cpp/functions/core/dashboard_panel.md @@ -0,0 +1,57 @@ +--- +name: dashboard_panel +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "bool dashboard_panel_begin(const char* title, float min_width = 200.0f, float min_height = 150.0f); void dashboard_panel_end()" +description: "Contenedor estilizado tipo panel para dashboards con titulo, bordes redondeados y tamaño minimo configurable" +tags: [imgui, panel, container, layout, dashboard] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/dashboard_panel.cpp" +framework: imgui +params: + - name: title + desc: "Titulo del panel, tambien sirve como ID de ImGui para distinguir multiples paneles" + - name: min_width + desc: "Ancho minimo del panel en pixels (0 = sin restriccion)" + - name: min_height + desc: "Alto minimo del panel en pixels (0 = sin restriccion)" +output: "true si el panel es visible y se debe renderizar contenido; llamar siempre dashboard_panel_end() independientemente del valor de retorno" +--- + +# dashboard_panel + +Panel estilizado para dashboards ImGui. Envuelve un `BeginChild`/`EndChild` con estilos predefinidos: fondo oscuro (`#1F1F26`), bordes redondeados (5 px), borde visible y separador bajo el titulo. + +## Uso + +```cpp +if (dashboard_panel_begin("Revenue", 300.0f, 200.0f)) { + line_plot("Revenue", xs, ys, N); +} +dashboard_panel_end(); // siempre llamar +``` + +## Implementacion + +- `PushStyleVar` aplica `ChildRounding = 5.0f` y `ChildBorderSize = 1.0f` +- `PushStyleColor` establece el fondo del child a `(0.12, 0.12, 0.15, 1.0)` +- `BeginChild` con `ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY` +- Titulo con `TextUnformatted` seguido de `Separator` +- `dashboard_panel_end` hace `EndChild`, `PopStyleColor(1)`, `PopStyleVar(2)` + +## Notas + +- El titulo actua como ID de ImGui: dos paneles con el mismo titulo en el mismo frame se comportan como uno solo. Usar `##` para diferenciar IDs si se repite el texto: `"Revenue##panel1"`. +- `AutoResizeY` hace que el panel crezca verticalmente con su contenido; `min_height` establece el piso. +- El patron begin/end es idomatico en ImGui: `end` debe llamarse siempre para hacer pop de los estilos, aunque `begin` retorne false. diff --git a/cpp/functions/core/docking_layout.cpp b/cpp/functions/core/docking_layout.cpp new file mode 100644 index 00000000..20a3f0ca --- /dev/null +++ b/cpp/functions/core/docking_layout.cpp @@ -0,0 +1,53 @@ +#include "core/docking_layout.h" +#include "imgui_internal.h" + +ImGuiID docking_layout(DockPreset preset) { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGuiID dockspace_id = ImGui::DockSpaceOverViewport(0, viewport); + + static bool initialized = false; + if (initialized) { + return dockspace_id; + } + initialized = true; + + if (preset == DockPreset::Default) { + return dockspace_id; + } + + ImGui::DockBuilderRemoveNode(dockspace_id); + ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size); + + if (preset == DockPreset::TwoColumns) { + ImGuiID right; + ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.6f, nullptr, &right); + + } else if (preset == DockPreset::ThreeColumns) { + ImGuiID center, right; + ImGuiID left_node; + ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.33f, &left_node, ¢er); + ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.5f, nullptr, &right); + + } else if (preset == DockPreset::SidebarLeft) { + ImGuiID main; + ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.25f, nullptr, &main); + + } else if (preset == DockPreset::SidebarRight) { + ImGuiID sidebar; + ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.25f, &sidebar, nullptr); + + } else if (preset == DockPreset::TopBottom) { + ImGuiID bottom; + ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.6f, nullptr, &bottom); + + } else if (preset == DockPreset::Dashboard) { + ImGuiID top, bottom; + ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.5f, &top, &bottom); + ImGui::DockBuilderSplitNode(top, ImGuiDir_Left, 0.5f, nullptr, nullptr); + ImGui::DockBuilderSplitNode(bottom, ImGuiDir_Left, 0.5f, nullptr, nullptr); + } + + ImGui::DockBuilderFinish(dockspace_id); + return dockspace_id; +} diff --git a/cpp/functions/core/docking_layout.h b/cpp/functions/core/docking_layout.h new file mode 100644 index 00000000..407aaa08 --- /dev/null +++ b/cpp/functions/core/docking_layout.h @@ -0,0 +1,16 @@ +#pragma once +#include "imgui.h" + +enum class DockPreset { + Default, // full dockspace, no preset splits + TwoColumns, // left 60% | right 40% + ThreeColumns, // left 33% | center 34% | right 33% + SidebarLeft, // sidebar 25% | main 75% + SidebarRight, // main 75% | sidebar 25% + TopBottom, // top 60% | bottom 40% + Dashboard // top-left | top-right | bottom-left | bottom-right (2x2 grid) +}; + +// Call once at the beginning of render_fn. +// Returns the dockspace ID (use with ImGui::SetNextWindowDockID if needed). +ImGuiID docking_layout(DockPreset preset = DockPreset::Default); diff --git a/cpp/functions/core/docking_layout.md b/cpp/functions/core/docking_layout.md new file mode 100644 index 00000000..1afbab70 --- /dev/null +++ b/cpp/functions/core/docking_layout.md @@ -0,0 +1,64 @@ +--- +name: docking_layout +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "ImGuiID docking_layout(DockPreset preset = DockPreset::Default)" +description: "Configura un docking space con presets de layout predefinidos para dashboards" +tags: [imgui, docking, layout, dashboard] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/docking_layout.cpp" +framework: imgui +params: + - name: preset + desc: "Layout predefinido a aplicar (Default, TwoColumns, ThreeColumns, SidebarLeft, SidebarRight, TopBottom, Dashboard)" +output: "ID del dockspace creado, usable con ImGui::SetNextWindowDockID" +--- + +# docking_layout + +Configura un docking space fullscreen sobre el viewport principal con presets de layout predefinidos. + +Usa `DockSpaceOverViewport` para crear el dockspace y, en el primer frame, aplica el preset con `DockBuilderSplitNode`. Llamar una vez al inicio de cada frame, antes de renderizar las ventanas hijas. + +## Presets disponibles + +| Preset | Layout | +|---|---| +| Default | Dockspace completo sin divisiones | +| TwoColumns | Izquierda 60% / Derecha 40% | +| ThreeColumns | Tres columnas iguales ~33% | +| SidebarLeft | Sidebar 25% / Main 75% | +| SidebarRight | Main 75% / Sidebar 25% | +| TopBottom | Arriba 60% / Abajo 40% | +| Dashboard | Grid 2x2 de cuatro paneles | + +## Ejemplo + +```cpp +void render_fn() { + ImGuiID dock = docking_layout(DockPreset::SidebarLeft); + + ImGui::Begin("Filters"); + // controles del sidebar + ImGui::End(); + + ImGui::Begin("Main"); + // contenido principal + ImGui::End(); +} +``` + +## Notas + +Requiere que `ImGuiConfigFlags_DockingEnable` este activo en `ImGui::GetIO().ConfigFlags` (habilitado por `app_base.cpp`). El preset se aplica solo en el primer frame (static bool). `imgui_internal.h` es necesario para `DockBuilder*`. diff --git a/cpp/functions/core/graph_spatial_hash.cpp b/cpp/functions/core/graph_spatial_hash.cpp new file mode 100644 index 00000000..1baebdc7 --- /dev/null +++ b/cpp/functions/core/graph_spatial_hash.cpp @@ -0,0 +1,174 @@ +#include "graph_spatial_hash.h" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static inline int floor_div(float v, float cell) { + return static_cast(std::floor(v / cell)); +} + +static inline float sq(float x) { return x * x; } + +// --------------------------------------------------------------------------- +// Snapshot de posiciones (almacenado internamente en build) +// La interfaz del header no pasa xs/ys a los metodos de query, por lo que +// SpatialHash conserva copias internas para calcular distancias. +// --------------------------------------------------------------------------- + +struct SpatialHashSnap { + float* xs = nullptr; + float* ys = nullptr; + float* sizes = nullptr; + int count = 0; +}; + +// Almacenamos el snapshot en memoria contigua al final del bloque de entries, +// para no añadir campos al header publico. Usamos un puntero opaco en un +// campo reservado. Como el header ya esta fijado, guardamos el snapshot como +// una variable estatica por instancia — aceptable para uso single-threaded +// tipico de ImGui. Para multi-instancia se puede usar un map. +// +// En la practica, la convencion del registry es que SpatialHash se crea una +// vez por frame de ImGui y se usa en el mismo hilo de render. + +static SpatialHashSnap g_snap; // snapshot mas reciente + +// --------------------------------------------------------------------------- +// SpatialHash +// --------------------------------------------------------------------------- + +int SpatialHash::cell_hash(int cx, int cy) const { + unsigned int h = static_cast(cx) * 73856093u + ^ static_cast(cy) * 19349663u; + return static_cast(h % static_cast(table_size)); +} + +SpatialHash::SpatialHash(float cell_size_, int table_size_) + : cell_size(cell_size_) + , table_size(table_size_) + , entry_count(0) + , entry_capacity(256) +{ + buckets = static_cast(std::malloc( + static_cast(table_size) * sizeof(int))); + + entries = static_cast(std::malloc( + static_cast(entry_capacity) * 2 * sizeof(int))); + + std::memset(buckets, -1, + static_cast(table_size) * sizeof(int)); +} + +SpatialHash::~SpatialHash() { + std::free(buckets); + std::free(entries); + + std::free(g_snap.xs); + std::free(g_snap.ys); + std::free(g_snap.sizes); + g_snap = {}; +} + +void SpatialHash::build(const float* xs, const float* ys, const float* sizes, int count) { + // --- Limpiar tabla --- + std::memset(buckets, -1, + static_cast(table_size) * sizeof(int)); + entry_count = 0; + + // --- Snapshot de posiciones para queries --- + if (g_snap.count < count) { + std::free(g_snap.xs); + std::free(g_snap.ys); + std::free(g_snap.sizes); + g_snap.xs = static_cast(std::malloc(static_cast(count) * sizeof(float))); + g_snap.ys = static_cast(std::malloc(static_cast(count) * sizeof(float))); + g_snap.sizes = static_cast(std::malloc(static_cast(count) * sizeof(float))); + } + std::memcpy(g_snap.xs, xs, static_cast(count) * sizeof(float)); + std::memcpy(g_snap.ys, ys, static_cast(count) * sizeof(float)); + std::memcpy(g_snap.sizes, sizes, static_cast(count) * sizeof(float)); + g_snap.count = count; + + // --- Insertar nodos en la tabla hash --- + for (int i = 0; i < count; ++i) { + int cx = floor_div(xs[i], cell_size); + int cy = floor_div(ys[i], cell_size); + int bucket = cell_hash(cx, cy); + + // Crecer entries si necesario + if (entry_count >= entry_capacity) { + entry_capacity *= 2; + entries = static_cast(std::realloc( + entries, + static_cast(entry_capacity) * 2 * sizeof(int))); + } + + // Insertar al frente de la cadena encadenada + int slot = entry_count++; + entries[slot * 2 + 0] = i; // node_index + entries[slot * 2 + 1] = buckets[bucket]; // next_in_chain + buckets[bucket] = slot; + } +} + +int SpatialHash::query_nearest(float qx, float qy, float radius, float* out_dist) const { + int best_idx = -1; + float best_d = radius; // umbral: solo aceptamos dentro del radio + + int qcx = floor_div(qx, cell_size); + int qcy = floor_div(qy, cell_size); + + // Escanear vecindad 3x3 de celdas + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + int bucket = cell_hash(qcx + dx, qcy + dy); + int slot = buckets[bucket]; + while (slot != -1) { + int ni = entries[slot * 2 + 0]; + float ex = g_snap.xs[ni] - qx; + float ey = g_snap.ys[ni] - qy; + float dist = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni]; + if (dist < best_d) { + best_d = dist; + best_idx = ni; + } + slot = entries[slot * 2 + 1]; + } + } + } + + if (out_dist && best_idx != -1) + *out_dist = best_d; + + return best_idx; +} + +int SpatialHash::query_radius(float qx, float qy, float radius, int* out, int max_out) const { + int found = 0; + + int qcx = floor_div(qx, cell_size); + int qcy = floor_div(qy, cell_size); + + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + int bucket = cell_hash(qcx + dx, qcy + dy); + int slot = buckets[bucket]; + while (slot != -1 && found < max_out) { + int ni = entries[slot * 2 + 0]; + float ex = g_snap.xs[ni] - qx; + float ey = g_snap.ys[ni] - qy; + float dist = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni]; + if (dist <= radius) + out[found++] = ni; + slot = entries[slot * 2 + 1]; + } + } + } + + return found; +} diff --git a/cpp/functions/core/graph_spatial_hash.h b/cpp/functions/core/graph_spatial_hash.h new file mode 100644 index 00000000..daf3411b --- /dev/null +++ b/cpp/functions/core/graph_spatial_hash.h @@ -0,0 +1,39 @@ +#pragma once +#include + +// SpatialHash — grid espacial para hit-testing de nodos en grafos. +// Permite buscar el nodo mas cercano a una posicion en O(1) amortizado. +struct SpatialHash { + float cell_size; + int table_size; // numero de buckets en la tabla hash + + // Almacenamiento interno (flat arrays) + int* buckets; // table_size entradas, cada una apunta al primer entry de la cadena + int* entries; // pares [node_index, next]: entries[2*i] = node_idx, entries[2*i+1] = next + int entry_count; + int entry_capacity; + + SpatialHash(float cell_size = 20.0f, int table_size = 4096); + ~SpatialHash(); + + SpatialHash(const SpatialHash&) = delete; + SpatialHash& operator=(const SpatialHash&) = delete; + + // Reconstruye la tabla desde arrays de posiciones y tamaños de nodos. + // xs[i], ys[i]: posicion del nodo i. sizes[i]: radio del nodo i. + void build(const float* xs, const float* ys, const float* sizes, int count); + + // Busca el punto mas cercano dentro de (qx, qy) con radio de busqueda dado. + // Descuenta el tamaño del nodo (circulo): distancia efectiva = dist(centro) - size. + // Retorna el indice del nodo, o -1 si no hay ninguno. + // Si out_dist es no-nulo, escribe la distancia al nodo encontrado. + int query_nearest(float qx, float qy, float radius, float* out_dist = nullptr) const; + + // Busca todos los puntos dentro del radio. Escribe indices en out[]. + // Retorna el numero de nodos encontrados (hasta max_out). + int query_radius(float qx, float qy, float radius, int* out, int max_out) const; + +private: + // Hash de coordenadas de celda enteras a indice de bucket. + int cell_hash(int cx, int cy) const; +}; diff --git a/cpp/functions/core/graph_spatial_hash.md b/cpp/functions/core/graph_spatial_hash.md new file mode 100644 index 00000000..6aaeb99d --- /dev/null +++ b/cpp/functions/core/graph_spatial_hash.md @@ -0,0 +1,71 @@ +--- +name: graph_spatial_hash +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "SpatialHash(float cell_size, int table_size)" +description: "Spatial hash grid para busqueda O(1) de puntos por posicion, usado para hit-testing de nodos en grafos" +tags: [spatial, hash, acceleration, graph, query, hittest] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/graph_spatial_hash.cpp" +framework: imgui +params: + - name: cell_size + desc: "Tamaño de cada celda del grid espacial (debe ser >= radio maximo de nodos)" + - name: table_size + desc: "Numero de buckets en la tabla hash (potencia de 2 recomendada para distribucion uniforme)" +output: "Estructura de hash espacial lista para queries de puntos cercanos" +--- + +# graph_spatial_hash + +Spatial hash grid para busqueda eficiente de nodos en grafos por posicion 2D. Util para hit-testing de nodos en editores de grafos basados en ImGui, donde se necesita saber que nodo esta bajo el cursor en cada frame. + +## Uso + +```cpp +SpatialHash sh(20.0f, 4096); + +// Reconstruir cada frame (o cuando cambian posiciones) +sh.build(node_xs, node_ys, node_sizes, node_count); + +// Hit-test: nodo mas cercano al cursor +float dist; +int hovered = sh.query_nearest(mouse_x, mouse_y, 30.0f, &dist); + +// Seleccion por area: todos los nodos dentro de un radio +int results[256]; +int count = sh.query_radius(center_x, center_y, radius, results, 256); +``` + +## Estructura interna + +- `buckets[table_size]`: cada entrada contiene el indice del primer slot de la cadena, o -1 si el bucket esta vacio. +- `entries[2 * entry_capacity]`: pares `[node_index, next_in_chain]` almacenados como flat array. Se expande con `realloc` si el numero de nodos supera la capacidad inicial (256). +- El snapshot de posiciones (`xs`, `ys`, `sizes`) se copia internamente en `build()` para que `query_nearest` y `query_radius` puedan calcular distancias sin recibir los arrays como parametro. + +## Hash function + +``` +hash(cx, cy) = (cx * 73856093 ^ cy * 19349663) % table_size +``` + +Donde `cx = floor(x / cell_size)`, `cy = floor(y / cell_size)`. + +## Notas + +- No es thread-safe: disenado para uso single-threaded en el hilo de render de ImGui. +- `build()` debe llamarse cada frame si las posiciones de los nodos cambian. +- `cell_size` debe ser >= al radio maximo de los nodos para que la vecindad 3x3 capture todos los candidatos posibles. +- La distancia efectiva al nodo se calcula como `dist(centro) - size`, de modo que el hit-test funciona correctamente para nodos de distintos tamaños. +- No implementa `operator=` ni copy constructor (deleted) para evitar doble-free de los arrays internos. diff --git a/cpp/functions/core/memory_overlay.cpp b/cpp/functions/core/memory_overlay.cpp new file mode 100644 index 00000000..a6e0d2bd --- /dev/null +++ b/cpp/functions/core/memory_overlay.cpp @@ -0,0 +1,99 @@ +#include "core/memory_overlay.h" +#include "imgui.h" + +#ifdef TRACY_ENABLE +#include "tracy/Tracy.hpp" +#endif + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma comment(lib, "psapi.lib") +#else +#include +#endif + +#include + +namespace { + +struct MemStats { + long rss_kb = -1; // Resident Set Size + long peak_kb = -1; // Peak RSS + long vsize_kb = -1; // Virtual memory size +}; + +static MemStats s_cached; +static double s_last_sample = -1.0; + +#ifdef _WIN32 +static MemStats sample_memory() { + MemStats s; + PROCESS_MEMORY_COUNTERS pmc{}; + if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) { + s.rss_kb = static_cast(pmc.WorkingSetSize / 1024); + s.peak_kb = static_cast(pmc.PeakWorkingSetSize / 1024); + s.vsize_kb = -1; // not easily available on Windows without VirtualQuery loop + } + return s; +} +#else +static MemStats sample_memory() { + MemStats s; + FILE* f = std::fopen("/proc/self/status", "r"); + if (!f) return s; + + char line[128]; + while (std::fgets(line, sizeof(line), f)) { + long val = 0; + if (std::sscanf(line, "VmRSS: %ld kB", &val) == 1) s.rss_kb = val; + if (std::sscanf(line, "VmPeak: %ld kB", &val) == 1) s.peak_kb = val; + if (std::sscanf(line, "VmSize: %ld kB", &val) == 1) s.vsize_kb = val; + } + std::fclose(f); + return s; +} +#endif + +} // namespace + +void memory_overlay() { +#ifdef TRACY_ENABLE + ZoneScoped; +#endif + + // Sample at most once per second + const double now = ImGui::GetTime(); + if (s_last_sample < 0.0 || (now - s_last_sample) >= 1.0) { + s_cached = sample_memory(); + s_last_sample = now; + } + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration + | ImGuiWindowFlags_AlwaysAutoResize + | ImGuiWindowFlags_NoSavedSettings + | ImGuiWindowFlags_NoFocusOnAppearing + | ImGuiWindowFlags_NoNav + | ImGuiWindowFlags_NoMove; + + const float pad = 10.0f; + const ImGuiViewport* vp = ImGui::GetMainViewport(); + ImVec2 pos(vp->WorkPos.x + vp->WorkSize.x - pad, + vp->WorkPos.y + vp->WorkSize.y - pad); + ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 1.0f)); + ImGui::SetNextWindowBgAlpha(0.65f); + + if (ImGui::Begin("##memory_overlay", nullptr, flags)) { + if (s_cached.rss_kb >= 0) { + ImGui::Text("RSS: %5ld MB", s_cached.rss_kb / 1024); + ImGui::Text("Peak: %5ld MB", s_cached.peak_kb / 1024); +#ifndef _WIN32 + ImGui::Text("VSize: %5ld MB", s_cached.vsize_kb / 1024); +#endif + } else { + ImGui::TextDisabled("Memory: N/A"); + } + } + ImGui::End(); +} diff --git a/cpp/functions/core/memory_overlay.h b/cpp/functions/core/memory_overlay.h new file mode 100644 index 00000000..13d37e1c --- /dev/null +++ b/cpp/functions/core/memory_overlay.h @@ -0,0 +1,7 @@ +#pragma once + +// Renders a memory statistics overlay in the bottom-right corner. +// Call within an ImGui frame once per frame. +// Samples /proc/self/status (Linux) or GetProcessMemoryInfo (Windows) at most +// once per second; results are cached between samples. +void memory_overlay(); diff --git a/cpp/functions/core/memory_overlay.md b/cpp/functions/core/memory_overlay.md new file mode 100644 index 00000000..c6f6ca22 --- /dev/null +++ b/cpp/functions/core/memory_overlay.md @@ -0,0 +1,56 @@ +--- +name: memory_overlay +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "void memory_overlay()" +description: "Renderiza un overlay de estadisticas de memoria (RSS, peak, vsize) en la esquina inferior derecha" +tags: [imgui, memory, overlay, debug, dashboard, profiling] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/memory_overlay.cpp" +framework: imgui +params: [] +output: "Renderiza el overlay de memoria en el frame ImGui actual" +--- + +# memory_overlay + +Muestra estadísticas de memoria del proceso en una ventana semi-transparente en la esquina inferior derecha. Complementa `fps_overlay` para un dashboard mínimo de rendimiento. + +Las métricas se leen como máximo una vez por segundo y se cachean en una variable estática local, evitando I/O excesivo en el hot path de render. + +## Métricas mostradas + +| Campo | Linux | Windows | +|---|---|---| +| RSS (MB) | `VmRSS` de `/proc/self/status` | `WorkingSetSize` (PSAPI) | +| Peak RSS (MB) | `VmPeak` de `/proc/self/status` | `PeakWorkingSetSize` (PSAPI) | +| Virtual Size (MB) | `VmSize` de `/proc/self/status` | N/A (no mostrado) | + +En Windows se requiere enlazar `psapi.lib` (el `.cpp` incluye `#pragma comment(lib, "psapi.lib")`). + +## Ejemplo + +```cpp +// En el loop de render, dentro de un frame ImGui: +fps_overlay(); +memory_overlay(); +``` + +## Notas + +- La función sigue la misma convención que `fps_overlay`: mismos flags de ventana, mismo alpha (0.65). +- Posición: esquina inferior derecha (`pivot = {1,1}`), separada 10 px del borde. +- La lectura de `/proc/self/status` es I/O local de ~0.1 ms, amortizada a 1 Hz — despreciable. +- Con `TRACY_ENABLE` activo, la función registra una zona `ZoneScoped` para que aparezca en el profiler. +- Si la plataforma no puede leer las estadísticas, muestra "Memory: N/A" en gris. diff --git a/cpp/functions/core/plot_theme.cpp b/cpp/functions/core/plot_theme.cpp new file mode 100644 index 00000000..ae4b5dbf --- /dev/null +++ b/cpp/functions/core/plot_theme.cpp @@ -0,0 +1,131 @@ +#include "plot_theme.h" + +// --------------------------------------------------------------------------- +// Preset structs +// --------------------------------------------------------------------------- + +PlotTheme plot_theme_preset_dark() { + PlotTheme t; + t.name = "dark"; + t.bg = ImVec4(0.10f, 0.10f, 0.12f, 1.00f); + t.frame_bg = ImVec4(0.14f, 0.14f, 0.17f, 1.00f); + t.text = ImVec4(0.88f, 0.88f, 0.90f, 1.00f); + t.grid = ImVec4(0.30f, 0.30f, 0.35f, 0.60f); + t.palette_count = 10; + t.palette[0] = ImVec4(0.33f, 0.62f, 0.91f, 1.00f); // steel blue + t.palette[1] = ImVec4(0.35f, 0.78f, 0.54f, 1.00f); // soft green + t.palette[2] = ImVec4(0.96f, 0.60f, 0.25f, 1.00f); // warm orange + t.palette[3] = ImVec4(0.85f, 0.38f, 0.42f, 1.00f); // muted red + t.palette[4] = ImVec4(0.72f, 0.52f, 0.88f, 1.00f); // lavender + t.palette[5] = ImVec4(0.38f, 0.80f, 0.82f, 1.00f); // teal + t.palette[6] = ImVec4(0.95f, 0.80f, 0.32f, 1.00f); // gold + t.palette[7] = ImVec4(0.60f, 0.82f, 0.38f, 1.00f); // lime + t.palette[8] = ImVec4(0.90f, 0.55f, 0.75f, 1.00f); // pink + t.palette[9] = ImVec4(0.55f, 0.70f, 0.95f, 1.00f); // periwinkle + return t; +} + +PlotTheme plot_theme_preset_light() { + PlotTheme t; + t.name = "light"; + t.bg = ImVec4(0.97f, 0.97f, 0.97f, 1.00f); + t.frame_bg = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + t.text = ImVec4(0.10f, 0.10f, 0.12f, 1.00f); + t.grid = ImVec4(0.70f, 0.70f, 0.72f, 0.60f); + t.palette_count = 10; + t.palette[0] = ImVec4(0.12f, 0.47f, 0.71f, 1.00f); // deep blue + t.palette[1] = ImVec4(0.17f, 0.63f, 0.17f, 1.00f); // deep green + t.palette[2] = ImVec4(0.84f, 0.37f, 0.00f, 1.00f); // burnt orange + t.palette[3] = ImVec4(0.84f, 0.15f, 0.16f, 1.00f); // vivid red + t.palette[4] = ImVec4(0.58f, 0.40f, 0.74f, 1.00f); // purple + t.palette[5] = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // cyan + t.palette[6] = ImVec4(0.74f, 0.74f, 0.13f, 1.00f); // olive + t.palette[7] = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // sky blue + t.palette[8] = ImVec4(0.89f, 0.47f, 0.76f, 1.00f); // rose + t.palette[9] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); // grey + return t; +} + +PlotTheme plot_theme_preset_high_contrast() { + PlotTheme t; + t.name = "high_contrast"; + t.bg = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + t.frame_bg = ImVec4(0.05f, 0.05f, 0.05f, 1.00f); + t.text = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + t.grid = ImVec4(0.30f, 0.30f, 0.30f, 0.80f); + t.palette_count = 10; + t.palette[0] = ImVec4(0.00f, 1.00f, 1.00f, 1.00f); // cyan neon + t.palette[1] = ImVec4(0.00f, 1.00f, 0.00f, 1.00f); // green neon + t.palette[2] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); // orange neon + t.palette[3] = ImVec4(1.00f, 0.00f, 0.50f, 1.00f); // magenta + t.palette[4] = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); // yellow + t.palette[5] = ImVec4(0.40f, 0.80f, 1.00f, 1.00f); // sky blue + t.palette[6] = ImVec4(1.00f, 0.40f, 0.40f, 1.00f); // salmon + t.palette[7] = ImVec4(0.60f, 1.00f, 0.40f, 1.00f); // lime + t.palette[8] = ImVec4(1.00f, 0.80f, 1.00f, 1.00f); // lavender bright + t.palette[9] = ImVec4(0.80f, 1.00f, 0.80f, 1.00f); // mint bright + return t; +} + +// --------------------------------------------------------------------------- +// Internal helper +// --------------------------------------------------------------------------- + +static void apply_imgui_colors(const PlotTheme& theme) { + ImGuiStyle& s = ImGui::GetStyle(); + s.Colors[ImGuiCol_WindowBg] = theme.bg; + s.Colors[ImGuiCol_ChildBg] = theme.bg; + s.Colors[ImGuiCol_PopupBg] = theme.frame_bg; + s.Colors[ImGuiCol_FrameBg] = theme.frame_bg; + s.Colors[ImGuiCol_FrameBgHovered] = theme.frame_bg; + s.Colors[ImGuiCol_FrameBgActive] = theme.frame_bg; + s.Colors[ImGuiCol_Text] = theme.text; + s.Colors[ImGuiCol_TextDisabled] = ImVec4(theme.text.x * 0.5f, + theme.text.y * 0.5f, + theme.text.z * 0.5f, + 0.80f); +} + +static void apply_implot_colors(const PlotTheme& theme) { + ImPlotStyle& ps = ImPlot::GetStyle(); + ps.Colors[ImPlotCol_PlotBg] = theme.bg; + ps.Colors[ImPlotCol_PlotBorder] = theme.frame_bg; + ps.Colors[ImPlotCol_FrameBg] = theme.frame_bg; + ps.Colors[ImPlotCol_LegendBg] = theme.frame_bg; + ps.Colors[ImPlotCol_LegendBorder] = theme.grid; + ps.Colors[ImPlotCol_LegendText] = theme.text; + ps.Colors[ImPlotCol_TitleText] = theme.text; + ps.Colors[ImPlotCol_InlayText] = theme.text; + ps.Colors[ImPlotCol_AxisText] = theme.text; + ps.Colors[ImPlotCol_AxisGrid] = theme.grid; + ps.Colors[ImPlotCol_AxisTick] = theme.grid; + + // Register custom colormap from palette + const int count = theme.palette_count > 10 ? 10 : theme.palette_count; + ImPlot::AddColormap(theme.name, theme.palette, count, false); + ImPlot::SetColormap(theme.name); +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +void plot_theme_apply(const PlotTheme& theme) { + apply_imgui_colors(theme); + apply_implot_colors(theme); +} + +void plot_theme_dark() { + PlotTheme t = plot_theme_preset_dark(); + plot_theme_apply(t); +} + +void plot_theme_light() { + PlotTheme t = plot_theme_preset_light(); + plot_theme_apply(t); +} + +void plot_theme_high_contrast() { + PlotTheme t = plot_theme_preset_high_contrast(); + plot_theme_apply(t); +} diff --git a/cpp/functions/core/plot_theme.h b/cpp/functions/core/plot_theme.h new file mode 100644 index 00000000..f8c82c5c --- /dev/null +++ b/cpp/functions/core/plot_theme.h @@ -0,0 +1,26 @@ +#pragma once +#include "imgui.h" +#include "implot.h" + +struct PlotTheme { + const char* name; + ImVec4 bg; // plot background + ImVec4 frame_bg; // frame background + ImVec4 text; // text color + ImVec4 grid; // grid lines + ImVec4 palette[10]; // color palette for series + int palette_count; +}; + +// Preset themes +void plot_theme_dark(); +void plot_theme_light(); +void plot_theme_high_contrast(); + +// Custom theme +void plot_theme_apply(const PlotTheme& theme); + +// Get preset theme structs (for inspection/modification) +PlotTheme plot_theme_preset_dark(); +PlotTheme plot_theme_preset_light(); +PlotTheme plot_theme_preset_high_contrast(); diff --git a/cpp/functions/core/plot_theme.md b/cpp/functions/core/plot_theme.md new file mode 100644 index 00000000..0c5864fc --- /dev/null +++ b/cpp/functions/core/plot_theme.md @@ -0,0 +1,96 @@ +--- +name: plot_theme +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "void plot_theme_dark() / void plot_theme_light() / void plot_theme_high_contrast() / void plot_theme_apply(const PlotTheme& theme)" +description: "Gestiona temas y paletas de colores para ImPlot e ImGui, con presets dark/light/high-contrast y soporte para temas custom" +tags: [theme, colors, palette, styling, dashboard] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui, implot] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/plot_theme.cpp" +framework: imgui +params: + - name: theme + desc: "Estructura PlotTheme con nombre, colores de fondo/frame/texto/grid y paleta de hasta 10 colores para series de datos" +output: "Aplica el tema al contexto ImPlot/ImGui actual modificando ImGuiStyle y ImPlotStyle en memoria" +--- + +# plot_theme + +Configura el aspecto visual de ventanas ImGui y graficas ImPlot en un solo paso. Tres presets listos para usar y una API de tema custom para casos especificos. + +## Presets + +### `plot_theme_dark()` + +Fondo oscuro (`#1A1A1F`) con paleta de 10 colores suaves: steel blue, soft green, warm orange, muted red, lavender, teal, gold, lime, pink, periwinkle. Ideal para dashboards de monitoreo. + +### `plot_theme_light()` + +Fondo claro (`#F7F7F7`) con colores vibrantes de alto contraste sobre blanco. Recomendado para capturas de pantalla o exportacion a documentos. + +### `plot_theme_high_contrast()` + +Fondo negro puro con colores neon: cyan, green, orange, magenta, yellow. Disenado para presentaciones en pantallas grandes o condiciones de poca luz. + +## Uso + +```cpp +#include "plot_theme.h" + +// Al inicio del frame, antes de BeginMainMenuBar / Begin +plot_theme_dark(); + +// O con tema custom +PlotTheme my_theme = plot_theme_preset_dark(); +my_theme.palette[0] = ImVec4(1.0f, 0.0f, 0.5f, 1.0f); // override primer color +plot_theme_apply(my_theme); +``` + +## API + +```cpp +// Presets de aplicacion directa +void plot_theme_dark(); +void plot_theme_light(); +void plot_theme_high_contrast(); + +// Tema custom +void plot_theme_apply(const PlotTheme& theme); + +// Obtener structs para inspeccion o modificacion parcial +PlotTheme plot_theme_preset_dark(); +PlotTheme plot_theme_preset_light(); +PlotTheme plot_theme_preset_high_contrast(); +``` + +## Estructura PlotTheme + +```cpp +struct PlotTheme { + const char* name; // nombre del colormap registrado en ImPlot + ImVec4 bg; // fondo del plot (ImPlotCol_PlotBg, ImGuiCol_WindowBg) + ImVec4 frame_bg; // fondo del frame (ImPlotCol_FrameBg, ImGuiCol_FrameBg) + ImVec4 text; // color del texto (todos los ImGui/ImPlot text cols) + ImVec4 grid; // lineas de cuadricula y ticks + ImVec4 palette[10]; // paleta de colores para series de datos + int palette_count; // numero de colores activos en la paleta (1-10) +}; +``` + +## Notas + +- `plot_theme_apply` llama a `ImPlot::AddColormap` y `ImPlot::SetColormap` con el campo `name` del tema como identificador. Si se llama dos veces con el mismo `name`, ImPlot registra el colormap de nuevo — usar nombres distintos si se crean multiples variantes en runtime. +- La funcion es "pure" en el sentido de que su unico efecto es escribir en `ImGuiStyle` e `ImPlotStyle` del contexto activo, sin I/O ni estado global propio. +- Requiere que `ImGui::CreateContext()` e `ImPlot::CreateContext()` hayan sido llamados antes del primer uso. +- Compatible con C++17. No usa excepciones ni RTTI. diff --git a/cpp/functions/core/sidebar.cpp b/cpp/functions/core/sidebar.cpp new file mode 100644 index 00000000..6ddb67a7 --- /dev/null +++ b/cpp/functions/core/sidebar.cpp @@ -0,0 +1,25 @@ +#include "core/sidebar.h" +#include "imgui.h" + +static bool s_sidebar_was_open = false; + +bool sidebar_begin(const char* title, bool* open, float width) { + if (!*open) { + s_sidebar_was_open = false; + if (ImGui::Button(">")) { + *open = true; + } + return false; + } + + s_sidebar_was_open = true; + ImGui::SetNextWindowSize(ImVec2(width, 0.0f), ImGuiCond_Always); + ImGui::Begin(title, open, ImGuiWindowFlags_NoCollapse); + return true; +} + +void sidebar_end() { + if (s_sidebar_was_open) { + ImGui::End(); + } +} diff --git a/cpp/functions/core/sidebar.h b/cpp/functions/core/sidebar.h new file mode 100644 index 00000000..ee43579b --- /dev/null +++ b/cpp/functions/core/sidebar.h @@ -0,0 +1,14 @@ +#pragma once + +// Collapsible sidebar panel. +// Usage: +// if (sidebar_begin("Filters", &sidebar_open)) { +// // draw filter controls +// } +// sidebar_end(); +// +// The sidebar renders as a fixed-width ImGui window. +// When collapsed, only a small toggle button is shown. + +bool sidebar_begin(const char* title, bool* open, float width = 250.0f); +void sidebar_end(); diff --git a/cpp/functions/core/sidebar.md b/cpp/functions/core/sidebar.md new file mode 100644 index 00000000..51e953bc --- /dev/null +++ b/cpp/functions/core/sidebar.md @@ -0,0 +1,59 @@ +--- +name: sidebar +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "bool sidebar_begin(const char* title, bool* open, float width = 250.0f)" +description: "Panel lateral colapsable para filtros y controles de dashboard" +tags: [imgui, sidebar, panel, layout, dashboard, controls] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/sidebar.cpp" +framework: imgui +params: + - name: title + desc: "Titulo del sidebar mostrado en la barra de titulo de la ventana" + - name: open + desc: "Puntero al estado abierto/cerrado; se pone a false si el usuario cierra la ventana" + - name: width + desc: "Ancho del sidebar en pixels (por defecto 250)" +output: "true si el sidebar esta abierto y se debe renderizar contenido entre sidebar_begin y sidebar_end" +--- + +# sidebar + +Panel lateral colapsable. Cuando `*open == true` renderiza una ventana ImGui de ancho fijo con boton de cierre. Cuando `*open == false` muestra un boton compacto ">" para reabrir. + +Siempre llamar `sidebar_end()` despues de `sidebar_begin()`, independientemente del valor de retorno. + +## Ejemplo + +```cpp +static bool filters_open = true; + +void render_fn() { + if (sidebar_begin("Filters", &filters_open)) { + ImGui::SliderFloat("Min value", &min_val, 0.0f, 100.0f); + ImGui::Checkbox("Show inactive", &show_inactive); + } + sidebar_end(); + + // contenido principal + ImGui::Begin("Main"); + // ... + ImGui::End(); +} +``` + +## Notas + +El estado de `s_sidebar_was_open` es una variable estatica interna que coordina `sidebar_begin` y `sidebar_end`. Solo un sidebar activo a la vez por frame (patron begin/end clasico de ImGui). Para multiples sidebars simultaneos, instanciar logica propia con estado separado. diff --git a/cpp/functions/core/tab_container.cpp b/cpp/functions/core/tab_container.cpp new file mode 100644 index 00000000..b6f0b9dc --- /dev/null +++ b/cpp/functions/core/tab_container.cpp @@ -0,0 +1,21 @@ +#include "core/tab_container.h" +#include "imgui.h" + +bool tab_container_begin(const char* id) { + return ImGui::BeginTabBar( + id, + ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown + ); +} + +bool tab_container_tab(const char* label) { + return ImGui::BeginTabItem(label); +} + +void tab_container_tab_end() { + ImGui::EndTabItem(); +} + +void tab_container_end() { + ImGui::EndTabBar(); +} diff --git a/cpp/functions/core/tab_container.h b/cpp/functions/core/tab_container.h new file mode 100644 index 00000000..e5413b0b --- /dev/null +++ b/cpp/functions/core/tab_container.h @@ -0,0 +1,20 @@ +#pragma once + +// Tab container — wraps ImGui::BeginTabBar with dashboard styling. +// Usage: +// if (tab_container_begin("##views")) { +// if (tab_container_tab("Overview")) { +// // draw overview content +// tab_container_tab_end(); +// } +// if (tab_container_tab("Details")) { +// // draw details content +// tab_container_tab_end(); +// } +// } +// tab_container_end(); + +bool tab_container_begin(const char* id); +bool tab_container_tab(const char* label); +void tab_container_tab_end(); +void tab_container_end(); diff --git a/cpp/functions/core/tab_container.md b/cpp/functions/core/tab_container.md new file mode 100644 index 00000000..1eebe084 --- /dev/null +++ b/cpp/functions/core/tab_container.md @@ -0,0 +1,64 @@ +--- +name: tab_container +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "bool tab_container_begin(const char* id)" +description: "Contenedor de tabs para organizar vistas multiples en un dashboard" +tags: [imgui, tabs, container, layout, dashboard] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/tab_container.cpp" +framework: imgui +params: + - name: id + desc: "Identificador unico del tab bar (ej: '##views'). Puede ser invisible con prefijo ##" + - name: label + desc: "Etiqueta visible de cada tab individual" +output: "tab_container_begin: true si el tab bar esta activo. tab_container_tab: true si el tab esta seleccionado y se debe renderizar su contenido" +--- + +# tab_container + +Wrapper fino sobre `ImGui::BeginTabBar` / `EndTabBar` con flags de dashboard preconfigurados: `Reorderable` (arrastrar tabs) y `FittingPolicyResizeDown` (tabs se achican si no caben). + +API de cuatro funciones siguiendo el patron begin/end de ImGui. Siempre llamar `tab_container_end()` si `tab_container_begin()` retorno true. Siempre llamar `tab_container_tab_end()` dentro del bloque `if (tab_container_tab(...))`. + +## Ejemplo + +```cpp +void render_fn() { + ImGui::Begin("Dashboard"); + + if (tab_container_begin("##main_tabs")) { + if (tab_container_tab("Overview")) { + render_overview(); + tab_container_tab_end(); + } + if (tab_container_tab("Details")) { + render_details(); + tab_container_tab_end(); + } + if (tab_container_tab("Settings")) { + render_settings(); + tab_container_tab_end(); + } + } + tab_container_end(); + + ImGui::End(); +} +``` + +## Notas + +Los flags `ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown` son fijos para estandarizar la experiencia de tabs en dashboards. Si se necesitan flags distintos usar `ImGui::BeginTabBar` directamente. diff --git a/cpp/functions/core/time_series_buffer.cpp b/cpp/functions/core/time_series_buffer.cpp new file mode 100644 index 00000000..ceac7c35 --- /dev/null +++ b/cpp/functions/core/time_series_buffer.cpp @@ -0,0 +1,90 @@ +#include "time_series_buffer.h" + +#include +#include +#include +#include +#include +#include + +TimeSeriesBuffer::TimeSeriesBuffer(size_t cap) + : data(new float[cap]), capacity(cap), count(0), offset(0) {} + +TimeSeriesBuffer::~TimeSeriesBuffer() { + delete[] data; +} + +TimeSeriesBuffer::TimeSeriesBuffer(TimeSeriesBuffer&& other) noexcept + : data(other.data), capacity(other.capacity), count(other.count), offset(other.offset) { + other.data = nullptr; + other.capacity = 0; + other.count = 0; + other.offset = 0; +} + +TimeSeriesBuffer& TimeSeriesBuffer::operator=(TimeSeriesBuffer&& other) noexcept { + if (this != &other) { + delete[] data; + data = other.data; + capacity = other.capacity; + count = other.count; + offset = other.offset; + other.data = nullptr; + other.capacity = 0; + other.count = 0; + other.offset = 0; + } + return *this; +} + +void TimeSeriesBuffer::push(float value) { + data[offset % capacity] = value; + offset++; + if (count < capacity) count++; +} + +float TimeSeriesBuffer::get(size_t index) const { + assert(index < count); + // oldest element is at (offset - count) % capacity + size_t real = (offset - count + index) % capacity; + return data[real]; +} + +float TimeSeriesBuffer::latest() const { + assert(count > 0); + return data[(offset - 1) % capacity]; +} + +float TimeSeriesBuffer::min() const { + float m = std::numeric_limits::max(); + for (size_t i = 0; i < count; ++i) m = std::min(m, get(i)); + return m; +} + +float TimeSeriesBuffer::max() const { + float m = std::numeric_limits::lowest(); + for (size_t i = 0; i < count; ++i) m = std::max(m, get(i)); + return m; +} + +float TimeSeriesBuffer::average() const { + if (count == 0) return 0.0f; + float sum = 0.0f; + for (size_t i = 0; i < count; ++i) sum += get(i); + return sum / static_cast(count); +} + +size_t TimeSeriesBuffer::size() const { + return count; +} + +bool TimeSeriesBuffer::full() const { + return count == capacity; +} + +size_t TimeSeriesBuffer::copy_ordered(float* out, size_t out_capacity) const { + size_t n = std::min(count, out_capacity); + for (size_t i = 0; i < n; ++i) + out[i] = get(i); + return n; +} diff --git a/cpp/functions/core/time_series_buffer.h b/cpp/functions/core/time_series_buffer.h new file mode 100644 index 00000000..d7f43c73 --- /dev/null +++ b/cpp/functions/core/time_series_buffer.h @@ -0,0 +1,31 @@ +#pragma once +#include + +struct TimeSeriesBuffer { + float* data; + size_t capacity; + size_t count; + size_t offset; // write head + + TimeSeriesBuffer(size_t cap); + ~TimeSeriesBuffer(); + + // Non-copyable, moveable + TimeSeriesBuffer(const TimeSeriesBuffer&) = delete; + TimeSeriesBuffer& operator=(const TimeSeriesBuffer&) = delete; + TimeSeriesBuffer(TimeSeriesBuffer&& other) noexcept; + TimeSeriesBuffer& operator=(TimeSeriesBuffer&& other) noexcept; + + void push(float value); + float get(size_t index) const; // 0 = oldest + float latest() const; + float min() const; + float max() const; + float average() const; + size_t size() const; + bool full() const; + + // For ImPlot: copies data in order to a contiguous array + // Returns actual count written + size_t copy_ordered(float* out, size_t out_capacity) const; +}; diff --git a/cpp/functions/core/time_series_buffer.md b/cpp/functions/core/time_series_buffer.md new file mode 100644 index 00000000..80a29d97 --- /dev/null +++ b/cpp/functions/core/time_series_buffer.md @@ -0,0 +1,59 @@ +--- +name: time_series_buffer +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "TimeSeriesBuffer(size_t capacity)" +description: "Ring buffer circular para datos de series temporales, optimizado para streaming de metricas en dashboards en tiempo real" +tags: [buffer, timeseries, streaming, dashboard, data] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/time_series_buffer.cpp" +framework: imgui +params: + - name: capacity + desc: "Numero maximo de muestras que almacena el buffer" +output: "Buffer circular listo para push de datos y lectura ordenada" +--- + +# time_series_buffer + +Ring buffer circular de floats para series temporales en tiempo real. Diseñado para alimentar plots de ImPlot con metricas de streaming (FPS, latencia, uso de CPU, etc.). + +## Uso basico + +```cpp +TimeSeriesBuffer fps_history(512); + +// En el loop principal: +fps_history.push(ImGui::GetIO().Framerate); + +// Para renderizar con ImPlot: +float ordered[512]; +size_t n = fps_history.copy_ordered(ordered, 512); +ImPlot::PlotLines("FPS", ordered, (int)n); +``` + +## Semantica del ring buffer + +- `push(v)` escribe en `data[offset % capacity]` y avanza el write head. +- `count` crece hasta `capacity` y se mantiene ahi — el buffer nunca desborda. +- `get(0)` retorna el elemento mas antiguo; `get(count-1)` el mas reciente. +- `latest()` es equivalente a `get(count-1)` pero mas explicito. +- `copy_ordered` extrae los datos en orden cronologico para pasarlos a ImPlot u otro consumidor que espere un array contiguo. + +## Notas + +- No depende de ImGui ni ImPlot directamente — es una estructura de datos pura. +- No es thread-safe. Para uso multihilo, proteger con mutex externo. +- Move semantics implementadas; copy queda eliminada (`= delete`) para evitar copias accidentales del heap. +- `min`, `max` y `average` iteran sobre `count` elementos — O(n). Para buffers grandes en hot paths, considerar mantener un acumulador incremental. diff --git a/cpp/functions/core/tracy_zone.cpp b/cpp/functions/core/tracy_zone.cpp new file mode 100644 index 00000000..7adf4ec3 --- /dev/null +++ b/cpp/functions/core/tracy_zone.cpp @@ -0,0 +1,5 @@ +// tracy_zone.cpp +// All definitions live in tracy_zone.h (macros + constexpr). +// This translation unit exists so the header can be included in any build +// without requiring Tracy — compile with -DTRACY_ENABLE to activate profiling. +#include "core/tracy_zone.h" diff --git a/cpp/functions/core/tracy_zone.h b/cpp/functions/core/tracy_zone.h new file mode 100644 index 00000000..7987c56c --- /dev/null +++ b/cpp/functions/core/tracy_zone.h @@ -0,0 +1,36 @@ +#pragma once +#include + +// Convenience macros for Tracy profiling zones. +// No-op when TRACY_ENABLE is not defined. +// Usage: FN_ZONE("my function") at the top of a scope. + +#ifdef TRACY_ENABLE +#include "tracy/Tracy.hpp" + +// Named zone (appears as-is in Tracy timeline) +#define FN_ZONE(name) ZoneScopedN(name) +// Named zone with explicit ARGB color +#define FN_ZONE_COLOR(name, color) ZoneScopedNC(name, color) +// Frame boundary marker +#define FN_FRAME_MARK FrameMark +// Plot a scalar value in Tracy's plot view +#define FN_PLOT(name, val) TracyPlot(name, val) + +#else + +#define FN_ZONE(name) (void)0 +#define FN_ZONE_COLOR(name, color) (void)0 +#define FN_FRAME_MARK (void)0 +#define FN_PLOT(name, val) (void)0 + +#endif + +// Preset ARGB colors for common zone categories. +namespace fn_tracy { + constexpr uint32_t COLOR_RENDER = 0x2196F3; // blue + constexpr uint32_t COLOR_UPDATE = 0x4CAF50; // green + constexpr uint32_t COLOR_IO = 0xFF9800; // orange + constexpr uint32_t COLOR_NETWORK = 0xF44336; // red + constexpr uint32_t COLOR_COMPUTE = 0x9C27B0; // purple +} diff --git a/cpp/functions/core/tracy_zone.md b/cpp/functions/core/tracy_zone.md new file mode 100644 index 00000000..8312ad9f --- /dev/null +++ b/cpp/functions/core/tracy_zone.md @@ -0,0 +1,83 @@ +--- +name: tracy_zone +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "FN_ZONE(name) / FN_ZONE_COLOR(name, color) / FN_FRAME_MARK / FN_PLOT(name, val)" +description: "Macros y constantes de conveniencia para Tracy profiling zones, compilables sin Tracy" +tags: [tracy, profiling, debug, performance, raii] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/tracy_zone.cpp" +framework: imgui +params: + - name: name + desc: "Nombre de la zona tal como aparece en la timeline de Tracy" + - name: color + desc: "Color ARGB uint32 de la zona en Tracy (opcional, solo FN_ZONE_COLOR)" +output: "Zona Tracy activa durante el scope actual; no-op cuando TRACY_ENABLE no está definido" +--- + +# tracy_zone + +Macros de scope para instrumentar secciones de código con Tracy Profiler. Cuando `TRACY_ENABLE` no está definido (ej. builds de producción) todas las macros se expanden a `(void)0`, sin coste alguno. + +## Macros disponibles + +| Macro | Descripción | +|---|---| +| `FN_ZONE("name")` | Zona con nombre, color automático | +| `FN_ZONE_COLOR("name", color)` | Zona con nombre y color ARGB explícito | +| `FN_FRAME_MARK` | Marca el límite de frame (eje X de Tracy) | +| `FN_PLOT("name", val)` | Envía un valor escalar al panel de plots | + +## Colores predefinidos (`fn_tracy::`) + +```cpp +fn_tracy::COLOR_RENDER // 0x2196F3 azul — rendering +fn_tracy::COLOR_UPDATE // 0x4CAF50 verde — game update / logic +fn_tracy::COLOR_IO // 0xFF9800 naranja — I/O disco/red +fn_tracy::COLOR_NETWORK // 0xF44336 rojo — red/HTTP +fn_tracy::COLOR_COMPUTE // 0x9C27B0 morado — compute / shader prep +``` + +## Ejemplo + +```cpp +#include "core/tracy_zone.h" + +void render_scene() { + FN_ZONE_COLOR("render_scene", fn_tracy::COLOR_RENDER); + // ... render calls ... +} + +void update(float dt) { + FN_ZONE("update"); + // ... game logic ... + FN_PLOT("dt_ms", dt * 1000.0f); +} + +void main_loop() { + while (running) { + update(dt); + render_scene(); + FN_FRAME_MARK; + } +} +``` + +## Notas + +- Compilar con `-DTRACY_ENABLE` y enlazar `TracyClient.cpp` para activar profiling. +- Sin `TRACY_ENABLE` el .cpp es prácticamente vacío — coste cero en producción. +- Los colores son ARGB; si el alpha es 0 Tracy aplica su color automático por zona. +- `FN_ZONE` expande a `ZoneScopedN(name)` de Tracy, que crea el contexto con `__LINE__` como discriminador — es seguro llamar varias veces en el mismo scope. diff --git a/cpp/functions/viz/candlestick.cpp b/cpp/functions/viz/candlestick.cpp new file mode 100644 index 00000000..508ee571 --- /dev/null +++ b/cpp/functions/viz/candlestick.cpp @@ -0,0 +1,81 @@ +#include "viz/candlestick.h" +#include "imgui.h" +#include "implot.h" + +void candlestick(const char* title, const double* dates, const double* opens, + const double* closes, const double* lows, const double* highs, + int count, float width_percent, bool tooltip) { + if (count <= 0) return; + + // Compute half-width of each candle body in data coordinates. + // Use spacing between consecutive dates when count > 1, else fallback to 0.5. + double spacing = (count > 1) ? (dates[1] - dates[0]) : 1.0; + double half_w = spacing * (double)width_percent * 0.5; + + ImPlot::SetupAxes("Date", "Price", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit); + ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Time); + + // Auto-fit X axis to the data range with a small margin. + ImPlot::SetupAxisLimits(ImAxis_X1, + dates[0] - spacing, + dates[count - 1] + spacing, + ImGuiCond_Always); + + if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) { + ImDrawList* draw = ImPlot::GetPlotDrawList(); + + const ImU32 col_bull = IM_COL32(0, 200, 80, 255); // green — close >= open + const ImU32 col_bear = IM_COL32(220, 50, 50, 255); // red — close < open + + int hovered_idx = -1; + + for (int i = 0; i < count; i++) { + double x = dates[i]; + double open = opens[i]; + double close = closes[i]; + double low = lows[i]; + double high = highs[i]; + + bool bullish = (close >= open); + ImU32 col = bullish ? col_bull : col_bear; + + // Convert data coordinates to screen pixels. + ImVec2 body_tl = ImPlot::PlotToPixels(x - half_w, bullish ? close : open); + ImVec2 body_br = ImPlot::PlotToPixels(x + half_w, bullish ? open : close); + ImVec2 wick_hi = ImPlot::PlotToPixels(x, high); + ImVec2 wick_lo = ImPlot::PlotToPixels(x, low); + float cx = (body_tl.x + body_br.x) * 0.5f; + + // Wick (high-low vertical line). + draw->AddLine(ImVec2(cx, wick_hi.y), ImVec2(cx, wick_lo.y), col, 1.5f); + + // Body rectangle (open-close). + // Ensure at least 1px height so flat candles are visible. + if (body_br.y <= body_tl.y + 1.0f) body_br.y = body_tl.y + 1.0f; + draw->AddRectFilled(body_tl, body_br, col); + draw->AddRect(body_tl, body_br, col); + + // Track hovered candle for tooltip. + if (tooltip && ImPlot::IsPlotHovered()) { + ImVec2 mouse = ImGui::GetMousePos(); + if (mouse.x >= body_tl.x - 4 && mouse.x <= body_br.x + 4 && + mouse.y >= wick_hi.y - 4 && mouse.y <= wick_lo.y + 4) { + hovered_idx = i; + } + } + } + + // Tooltip for the hovered candle. + if (tooltip && hovered_idx >= 0) { + int i = hovered_idx; + ImGui::BeginTooltip(); + ImGui::Text("O: %.4f", opens[i]); + ImGui::Text("H: %.4f", highs[i]); + ImGui::Text("L: %.4f", lows[i]); + ImGui::Text("C: %.4f", closes[i]); + ImGui::EndTooltip(); + } + + ImPlot::EndPlot(); + } +} diff --git a/cpp/functions/viz/candlestick.h b/cpp/functions/viz/candlestick.h new file mode 100644 index 00000000..e669b450 --- /dev/null +++ b/cpp/functions/viz/candlestick.h @@ -0,0 +1,9 @@ +#pragma once + +// Renders an OHLC candlestick chart using ImPlot custom rendering. +// Call within an ImGui frame (inside fn::run_app render callback). +// Green candles when close >= open, red when close < open. +// width_percent controls candle body width as a fraction of inter-candle spacing. +void candlestick(const char* title, const double* dates, const double* opens, + const double* closes, const double* lows, const double* highs, + int count, float width_percent = 0.25f, bool tooltip = true); diff --git a/cpp/functions/viz/candlestick.md b/cpp/functions/viz/candlestick.md new file mode 100644 index 00000000..979d266e --- /dev/null +++ b/cpp/functions/viz/candlestick.md @@ -0,0 +1,67 @@ +--- +name: candlestick +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "void candlestick(const char* title, const double* dates, const double* opens, const double* closes, const double* lows, const double* highs, int count, float width_percent = 0.25f, bool tooltip = true)" +description: "Renderiza un grafico de velas OHLC usando ImPlot custom rendering. Verde para velas alcistas (close >= open), rojo para bajistas." +tags: [implot, chart, visualization, gpu, candlestick, ohlc, finance] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [implot, imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/candlestick.cpp" +framework: imgui +params: + - name: title + desc: "Titulo del grafico, se muestra como header del plot" + - name: dates + desc: "Array de timestamps Unix o indices numericos del eje X, uno por vela" + - name: opens + desc: "Array de precios de apertura, uno por vela" + - name: closes + desc: "Array de precios de cierre, uno por vela" + - name: lows + desc: "Array de precios minimos (punta inferior del wick), uno por vela" + - name: highs + desc: "Array de precios maximos (punta superior del wick), uno por vela" + - name: count + desc: "Numero de velas (longitud de todos los arrays)" + - name: width_percent + desc: "Ancho del body de cada vela como fraccion del espacio entre puntos consecutivos (0.0-1.0, default 0.25)" + - name: tooltip + desc: "Si true, muestra tooltip con valores O/H/L/C al hacer hover sobre una vela" +output: "Renderiza el grafico de velas OHLC en el frame ImGui actual, sin retornar valor" +--- + +# candlestick + +Grafico de velas OHLC completo usando custom rendering de ImPlot. Dibuja body (open-close) y wicks (high-low) por vela usando `ImPlot::GetPlotDrawList()` y `ImPlot::PlotToPixels()` para conversion de coordenadas. + +Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). El eje X se configura con `ImPlotScale_Time` para timestamps Unix. + +Solo tiene overload `double` porque los datos financieros requieren doble precision. + +## Ejemplo + +```cpp +// arrays de datos financieros (timestamps Unix, precios) +candlestick("BTC/USD", dates, opens, closes, lows, highs, 90); + +// sin tooltip, velas mas anchas +candlestick("ETH/USD", dates, opens, closes, lows, highs, 30, 0.6f, false); +``` + +## Notas + +- El ancho de cada vela se calcula como `(dates[1] - dates[0]) * width_percent * 0.5` en cada lado. Asume spacing uniforme entre velas. +- Para un solo punto (`count == 1`) el spacing por defecto es 1.0. +- La deteccion de hover usa un margen de 4px alrededor del area cuerpo+wick para facilitar la interaccion. +- El eje X usa `ImPlotScale_Time` — si los datos son indices numericos simples (0, 1, 2...) en lugar de timestamps, pasar `ImPlotAxisFlags_NoDecorations` o cambiar `SetupAxisScale`. diff --git a/cpp/functions/viz/gauge.cpp b/cpp/functions/viz/gauge.cpp new file mode 100644 index 00000000..a39752ed --- /dev/null +++ b/cpp/functions/viz/gauge.cpp @@ -0,0 +1,94 @@ +#include "viz/gauge.h" +#include "imgui.h" +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846f +#endif + +void gauge(const char* label, float value, float min_val, float max_val, float radius) { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 pos = ImGui::GetCursorScreenPos(); + + // Reserve space: diameter + label line + float diameter = radius * 2.0f; + ImGui::Dummy(ImVec2(diameter, diameter + ImGui::GetTextLineHeightWithSpacing())); + + ImVec2 center = ImVec2(pos.x + radius, pos.y + radius); + + // Arc spans 240 degrees: from 150deg to 390deg (i.e. 150 to 30 going clockwise) + // In screen space Y is down, so angles go clockwise. + // Start angle: 150 degrees = bottom-left, End angle: 390 = 30 degrees = bottom-right + const float angle_start = (150.0f * (float)M_PI) / 180.0f; + const float angle_end = (390.0f * (float)M_PI) / 180.0f; + const int num_segments = 64; + + // Background arc (dark gray) + ImU32 bg_color = IM_COL32(60, 60, 60, 220); + for (int i = 0; i < num_segments; i++) { + float a0 = angle_start + (angle_end - angle_start) * ((float)i / num_segments); + float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments); + draw_list->AddLine( + ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius), + ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius), + bg_color, 6.0f); + } + + // Normalize value to [0, 1] + float t = 0.0f; + if (max_val > min_val) { + t = (value - min_val) / (max_val - min_val); + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + } + + // Color: green (t=0) -> yellow (t=0.5) -> red (t=1) + float r, g, b; + if (t < 0.5f) { + float s = t * 2.0f; + r = (unsigned char)(s * 255); + g = 200; + b = 0; + } else { + float s = (t - 0.5f) * 2.0f; + r = 220; + g = (unsigned char)((1.0f - s) * 200); + b = 0; + } + ImU32 value_color = IM_COL32((int)r, (int)g, (int)b, 255); + + // Value arc + float angle_value = angle_start + (angle_end - angle_start) * t; + int value_segments = (int)(num_segments * t); + for (int i = 0; i < value_segments; i++) { + float a0 = angle_start + (angle_end - angle_start) * ((float)i / num_segments); + float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments); + draw_list->AddLine( + ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius), + ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius), + value_color, 6.0f); + } + + // Needle: line from center to arc at current angle + float needle_len = radius * 0.75f; + ImVec2 needle_tip = ImVec2( + center.x + cosf(angle_value) * needle_len, + center.y + sinf(angle_value) * needle_len); + draw_list->AddLine(center, needle_tip, IM_COL32(255, 255, 255, 240), 2.0f); + draw_list->AddCircleFilled(center, 4.0f, IM_COL32(255, 255, 255, 200)); + + // Value text centered + char val_buf[32]; + snprintf(val_buf, sizeof(val_buf), "%.1f", value); + ImVec2 val_size = ImGui::CalcTextSize(val_buf); + draw_list->AddText( + ImVec2(center.x - val_size.x * 0.5f, center.y + radius * 0.35f), + IM_COL32(230, 230, 230, 255), val_buf); + + // Label below value + ImVec2 label_size = ImGui::CalcTextSize(label); + draw_list->AddText( + ImVec2(center.x - label_size.x * 0.5f, center.y + radius * 0.35f + val_size.y + 2.0f), + IM_COL32(180, 180, 180, 200), label); +} diff --git a/cpp/functions/viz/gauge.h b/cpp/functions/viz/gauge.h new file mode 100644 index 00000000..219d3ce3 --- /dev/null +++ b/cpp/functions/viz/gauge.h @@ -0,0 +1,6 @@ +#pragma once + +// Renders a circular gauge/speedometer indicator using ImGui draw primitives. +// Call within an ImGui frame. +// color is interpolated green->yellow->red based on normalized value. +void gauge(const char* label, float value, float min_val, float max_val, float radius = 60.0f); diff --git a/cpp/functions/viz/gauge.md b/cpp/functions/viz/gauge.md new file mode 100644 index 00000000..793e8037 --- /dev/null +++ b/cpp/functions/viz/gauge.md @@ -0,0 +1,59 @@ +--- +name: gauge +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "void gauge(const char* label, float value, float min_val, float max_val, float radius = 60.0f)" +description: "Renderiza un indicador circular tipo gauge/velocimetro usando ImGui draw primitives" +tags: [imgui, visualization, gauge, kpi, dashboard] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/gauge.cpp" +framework: imgui +params: + - name: label + desc: "Etiqueta del gauge, se muestra centrada debajo del valor numerico" + - name: value + desc: "Valor actual a mostrar en el gauge" + - name: min_val + desc: "Valor minimo de la escala (extremo izquierdo del arco)" + - name: max_val + desc: "Valor maximo de la escala (extremo derecho del arco)" + - name: radius + desc: "Radio del gauge en pixels (default 60.0)" +output: "Renderiza el gauge en el frame ImGui actual, reservando espacio con ImGui::Dummy" +--- + +# gauge + +Indicador circular tipo gauge/velocimetro construido sobre ImGui draw primitives. No requiere ImPlot. + +El arco ocupa 240 grados (de 150deg a 390deg en sentido horario). El color del arco de valor interpolado de verde (minimo) a amarillo (mitad) a rojo (maximo). Una aguja blanca apunta al valor actual. + +Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). + +## Ejemplo + +```cpp +// KPI card con gauge de temperatura +gauge("CPU Temp", 72.5f, 0.0f, 100.0f, 50.0f); + +// Gauge grande para dashboard principal +gauge("Velocidad", 3200.0f, 0.0f, 5000.0f, 80.0f); +``` + +## Notas + +- El arco de fondo es gris oscuro (IM_COL32(60,60,60,220)), 6px de grosor. +- La aguja tiene longitud del 75% del radio para evitar solapar el arco. +- Usa solo `float`; no ofrece overload `double` porque ImGui DrawList trabaja en coordenadas de pantalla (float). +- El espacio reservado es `diameter x (diameter + line_height)` para incluir la etiqueta. diff --git a/cpp/functions/viz/graph_force_layout.cpp b/cpp/functions/viz/graph_force_layout.cpp new file mode 100644 index 00000000..52b39752 --- /dev/null +++ b/cpp/functions/viz/graph_force_layout.cpp @@ -0,0 +1,353 @@ +#include "viz/graph_force_layout.h" +#include "viz/graph_types.h" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// Quadtree for Barnes-Hut approximation +// --------------------------------------------------------------------------- + +struct QuadNode { + float cx, cy; // center of mass + float mass; // total mass (node count in subtree) + float x0, y0; // bounding box min + float x1, y1; // bounding box max + int children[4]; // NW=0, NE=1, SW=2, SE=3 (-1 = empty) + int body; // node index if leaf (-1 if internal) +}; + +static constexpr int MAX_QUAD_NODES = 1 << 20; // supports graphs up to ~1M nodes +static QuadNode quad_pool[MAX_QUAD_NODES]; +static int quad_count = 0; + +static int quad_new(float x0, float y0, float x1, float y1) { + if (quad_count >= MAX_QUAD_NODES) return -1; + int idx = quad_count++; + QuadNode& q = quad_pool[idx]; + q.cx = 0; q.cy = 0; q.mass = 0; + q.x0 = x0; q.y0 = y0; q.x1 = x1; q.y1 = y1; + q.children[0] = q.children[1] = q.children[2] = q.children[3] = -1; + q.body = -1; + return idx; +} + +// Determine quadrant index for point (px,py) relative to cell midpoint. +// 0=NW, 1=NE, 2=SW, 3=SE +static int quad_child_idx(const QuadNode& q, float px, float py) { + float mx = (q.x0 + q.x1) * 0.5f; + float my = (q.y0 + q.y1) * 0.5f; + int xi = (px >= mx) ? 1 : 0; + int yi = (py >= my) ? 2 : 0; + return xi | yi; +} + +// Subdivide cell qi into four children. +static void quad_subdivide(int qi) { + QuadNode& q = quad_pool[qi]; + float mx = (q.x0 + q.x1) * 0.5f; + float my = (q.y0 + q.y1) * 0.5f; + // NW + quad_pool[qi].children[0] = quad_new(q.x0, q.y0, mx, my); + // NE + quad_pool[qi].children[1] = quad_new(mx, q.y0, q.x1, my); + // SW + quad_pool[qi].children[2] = quad_new(q.x0, my, mx, q.y1); + // SE + quad_pool[qi].children[3] = quad_new(mx, my, q.x1, q.y1); +} + +// Insert body (node_idx at position nx,ny with mass nmass) into cell qi. +// Uses iterative descent to avoid stack overflow on deep trees. +static void quad_insert(int root, int node_idx, float nx, float ny, float nmass) { + int qi = root; + while (qi >= 0) { + QuadNode& q = quad_pool[qi]; + // Update center of mass + float total = q.mass + nmass; + q.cx = (q.cx * q.mass + nx * nmass) / total; + q.cy = (q.cy * q.mass + ny * nmass) / total; + q.mass = total; + + if (q.body == -1 && q.children[0] == -1) { + // Empty leaf: place body here + q.body = node_idx; + return; + } + + if (q.body >= 0) { + // Leaf with existing body: subdivide, push existing body down + quad_subdivide(qi); + // Move old body into correct child (re-read q after subdivide since pool may shift) + QuadNode& qq = quad_pool[qi]; + int old_body = qq.body; + float obx = /* we need positions */ 0, oby = 0; + // We store positions in the GraphData, pass via closure is not possible here. + // Instead we pass a pointer to positions alongside. We'll fix this by using + // a file-scope pointer set before each build. + (void)old_body; (void)obx; (void)oby; + // NOTE: positions accessed via file-scope g_nodes pointer below. + qq.body = -1; + } + + int ci = quad_child_idx(quad_pool[qi], nx, ny); + qi = quad_pool[qi].children[ci]; + } +} + +// File-scope pointers set before each tree build (avoids passing them everywhere). +static const GraphNode* g_nodes = nullptr; + +// Insert body knowing positions from g_nodes. +static void quad_insert_body(int qi, int node_idx) { + float nx = g_nodes[node_idx].x; + float ny = g_nodes[node_idx].y; + const float nmass = 1.0f; + + while (qi >= 0) { + QuadNode& q = quad_pool[qi]; + float total = q.mass + nmass; + q.cx = (q.cx * q.mass + nx * nmass) / total; + q.cy = (q.cy * q.mass + ny * nmass) / total; + q.mass = total; + + if (q.body == -1 && q.children[0] == -1) { + // Empty leaf + q.body = node_idx; + return; + } + + if (q.children[0] == -1) { + // Leaf occupied: subdivide and push existing body down + int old_body = q.body; + q.body = -1; + quad_subdivide(qi); + + // Push old body into child + int old_ci = quad_child_idx(quad_pool[qi], g_nodes[old_body].x, g_nodes[old_body].y); + int old_child = quad_pool[qi].children[old_ci]; + if (old_child >= 0) { + QuadNode& oc = quad_pool[old_child]; + oc.cx = g_nodes[old_body].x; + oc.cy = g_nodes[old_body].y; + oc.mass = 1.0f; + oc.body = old_body; + } + } + + int ci = quad_child_idx(quad_pool[qi], nx, ny); + qi = quad_pool[qi].children[ci]; + } +} + +// Compute Barnes-Hut repulsion force on node at (nx,ny) from subtree qi. +// Accumulates force into (fx, fy). +static void quad_force(int qi, float nx, float ny, + float theta, float repulsion, float min_dist, + float& fx, float& fy) { + // Iterative traversal using a small stack to avoid recursion depth issues. + static int stack[MAX_QUAD_NODES]; // reuse static stack + int top = 0; + stack[top++] = qi; + + while (top > 0) { + int ci = stack[--top]; + if (ci < 0) continue; + const QuadNode& q = quad_pool[ci]; + if (q.mass == 0) continue; + + float dx = q.cx - nx; + float dy = q.cy - ny; + float dist2 = dx * dx + dy * dy; + float dist = std::sqrt(dist2); + if (dist < min_dist) dist = min_dist; + + // Cell size + float cell_size = q.x1 - q.x0; + + // Use multipole approximation if far enough OR if leaf + bool is_leaf = (q.children[0] == -1); + if (is_leaf || (cell_size / dist) < theta) { + // Coulomb repulsion: F = repulsion * mass / dist^2 + float force = repulsion * q.mass / (dist * dist); + fx -= force * dx / dist; + fy -= force * dy / dist; + } else { + // Push children + for (int k = 0; k < 4; ++k) { + if (q.children[k] >= 0) + stack[top++] = q.children[k]; + } + } + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config) { + if (graph.node_count <= 0) return 0.0f; + + // Temporary force accumulators (stack-allocated for small graphs, static for large) + static float* fx_buf = nullptr; + static float* fy_buf = nullptr; + static int buf_cap = 0; + + if (graph.node_count > buf_cap) { + delete[] fx_buf; + delete[] fy_buf; + buf_cap = graph.node_count + 64; + fx_buf = new float[buf_cap]; + fy_buf = new float[buf_cap]; + } + + float total_energy = 0.0f; + + for (int iter = 0; iter < config.iterations; ++iter) { + // Zero forces + for (int i = 0; i < graph.node_count; ++i) { + fx_buf[i] = 0.0f; + fy_buf[i] = 0.0f; + } + + // ---- Build Barnes-Hut quadtree ---- + // Compute bounding box of current positions + float bx0 = graph.nodes[0].x, bx1 = graph.nodes[0].x; + float by0 = graph.nodes[0].y, by1 = graph.nodes[0].y; + for (int i = 1; i < graph.node_count; ++i) { + float px = graph.nodes[i].x, py = graph.nodes[i].y; + if (px < bx0) bx0 = px; if (px > bx1) bx1 = px; + if (py < by0) by0 = py; if (py > by1) by1 = py; + } + // Add margin to avoid degeneracies + float margin = (bx1 - bx0 + by1 - by0) * 0.05f + 1.0f; + bx0 -= margin; bx1 += margin; + by0 -= margin; by1 += margin; + // Make it square + float side = std::max(bx1 - bx0, by1 - by0); + float cx = (bx0 + bx1) * 0.5f, cy = (by0 + by1) * 0.5f; + bx0 = cx - side * 0.5f; bx1 = cx + side * 0.5f; + by0 = cy - side * 0.5f; by1 = cy + side * 0.5f; + + quad_count = 0; + g_nodes = graph.nodes; + int root = quad_new(bx0, by0, bx1, by1); + + for (int i = 0; i < graph.node_count; ++i) { + quad_insert_body(root, i); + } + + // ---- Repulsion via Barnes-Hut ---- + for (int i = 0; i < graph.node_count; ++i) { + if (graph.nodes[i].pinned) continue; + quad_force(root, + graph.nodes[i].x, graph.nodes[i].y, + config.theta, config.repulsion, config.min_distance, + fx_buf[i], fy_buf[i]); + // Subtract self-interaction (the tree includes the node itself) + // Self-force: repulsion * 1 / min_dist^2, but direction is (0,0) -> skip + } + + // ---- Attraction along edges (spring force) ---- + for (int e = 0; e < graph.edge_count; ++e) { + const GraphEdge& edge = graph.edges[e]; + int s = (int)edge.source; + int t = (int)edge.target; + if (s < 0 || s >= graph.node_count) continue; + if (t < 0 || t >= graph.node_count) continue; + + float dx = graph.nodes[t].x - graph.nodes[s].x; + float dy = graph.nodes[t].y - graph.nodes[s].y; + float dist = std::sqrt(dx * dx + dy * dy); + if (dist < config.min_distance) dist = config.min_distance; + + // F = k * dist * weight (Hooke: pulls toward equilibrium at 0) + float force = config.attraction * dist * edge.weight; + float fx_e = force * dx / dist; + float fy_e = force * dy / dist; + + if (!graph.nodes[s].pinned) { fx_buf[s] += fx_e; fy_buf[s] += fy_e; } + if (!graph.nodes[t].pinned) { fx_buf[t] -= fx_e; fy_buf[t] -= fy_e; } + } + + // ---- Gravity toward center (0,0) ---- + if (config.gravity != 0.0f) { + for (int i = 0; i < graph.node_count; ++i) { + if (graph.nodes[i].pinned) continue; + fx_buf[i] -= config.gravity * graph.nodes[i].x; + fy_buf[i] -= config.gravity * graph.nodes[i].y; + } + } + + // ---- Integrate: v = v * damping + F; pos += v ---- + total_energy = 0.0f; + for (int i = 0; i < graph.node_count; ++i) { + GraphNode& n = graph.nodes[i]; + if (n.pinned) continue; + + n.vx = n.vx * config.damping + fx_buf[i]; + n.vy = n.vy * config.damping + fy_buf[i]; + + // Clamp velocity + n.vx = std::max(-config.max_velocity, std::min(config.max_velocity, n.vx)); + n.vy = std::max(-config.max_velocity, std::min(config.max_velocity, n.vy)); + + n.x += n.vx; + n.y += n.vy; + + total_energy += n.vx * n.vx + n.vy * n.vy; + } + } + + graph.update_bounds(); + return total_energy; +} + +void graph_force_layout_reset(GraphData& graph, float spread) { + for (int i = 0; i < graph.node_count; ++i) { + GraphNode& n = graph.nodes[i]; + if (n.pinned) continue; + // rand() produces [0, RAND_MAX]; map to [-spread, spread] + n.x = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f); + n.y = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f); + n.vx = 0.0f; + n.vy = 0.0f; + } + graph.update_bounds(); +} + +void graph_layout_circular(GraphData& graph, float radius) { + if (graph.node_count <= 0) return; + const float two_pi = 6.28318530718f; + for (int i = 0; i < graph.node_count; ++i) { + GraphNode& n = graph.nodes[i]; + if (n.pinned) continue; + float angle = two_pi * (float)i / (float)graph.node_count; + n.x = radius * std::cos(angle); + n.y = radius * std::sin(angle); + n.vx = 0.0f; + n.vy = 0.0f; + } + graph.update_bounds(); +} + +void graph_layout_grid(GraphData& graph, float spacing) { + if (graph.node_count <= 0) return; + int cols = (int)std::ceil(std::sqrt((float)graph.node_count)); + int rows = (graph.node_count + cols - 1) / cols; + float ox = -0.5f * (cols - 1) * spacing; + float oy = -0.5f * (rows - 1) * spacing; + for (int i = 0; i < graph.node_count; ++i) { + GraphNode& n = graph.nodes[i]; + if (n.pinned) continue; + int col = i % cols; + int row = i / cols; + n.x = ox + col * spacing; + n.y = oy + row * spacing; + n.vx = 0.0f; + n.vy = 0.0f; + } + graph.update_bounds(); +} diff --git a/cpp/functions/viz/graph_force_layout.h b/cpp/functions/viz/graph_force_layout.h new file mode 100644 index 00000000..bde0b0e3 --- /dev/null +++ b/cpp/functions/viz/graph_force_layout.h @@ -0,0 +1,27 @@ +#pragma once + +struct GraphData; // forward declare + +struct ForceLayoutConfig { + float repulsion = 500.0f; // repulsion strength between all nodes + float attraction = 0.01f; // spring constant for edges + float damping = 0.85f; // velocity decay per step + float min_distance = 1.0f; // minimum distance (avoid division by zero) + float theta = 0.5f; // Barnes-Hut threshold (0 = exact, 1 = fast) + float gravity = 0.1f; // pull toward center (prevents drift) + float max_velocity = 50.0f; // cap velocity per axis + int iterations = 1; // steps per call +}; + +// Perform one (or more) steps of force-directed layout. +// Modifies node positions (x, y) and velocities (vx, vy) in-place. +// Returns the total kinetic energy (sum of |v|^2). When energy < threshold, +// layout has converged. +float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config = {}); + +// Reset: randomize positions within [-spread, spread], zero velocities. +void graph_force_layout_reset(GraphData& graph, float spread = 200.0f); + +// Preset layouts (non-iterative, instant positioning) +void graph_layout_circular(GraphData& graph, float radius = 100.0f); +void graph_layout_grid(GraphData& graph, float spacing = 20.0f); diff --git a/cpp/functions/viz/graph_force_layout.md b/cpp/functions/viz/graph_force_layout.md new file mode 100644 index 00000000..679ad9d3 --- /dev/null +++ b/cpp/functions/viz/graph_force_layout.md @@ -0,0 +1,79 @@ +--- +name: graph_force_layout +kind: function +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)" +description: "Layout force-directed con aproximacion Barnes-Hut para grafos grandes, ejecuta un paso de simulacion por llamada" +tags: [graph, layout, force-directed, barnes-hut, physics, gpu] +uses_functions: [] +uses_types: ["GraphData_cpp_viz"] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/graph_force_layout.cpp" +framework: imgui +params: + - name: graph + desc: "Referencia al grafo (GraphData) cuyos nodos se actualizan in-place. Modifica x, y, vx, vy de cada nodo no pinned." + - name: config + desc: "Parametros de la simulacion: repulsion (fuerza coulombiana), attraction (spring constant), damping (decay de velocidad), theta (precision Barnes-Hut 0=exacto/1=rapido), gravity (atraccion al centro), max_velocity, iterations." +output: "Energia cinetica total (suma de |v|^2). Cuando cae por debajo de un umbral elegido por el caller, el layout ha convergido y se puede dejar de llamar." +--- + +# graph_force_layout + +Implementa el algoritmo de layout force-directed clasico (Fruchterman-Reingold / Eades) con aproximacion Barnes-Hut O(n log n) para escalar a grafos de miles de nodos. + +## Algoritmo + +Cada llamada a `graph_force_layout_step` ejecuta `config.iterations` pasos. Un paso: + +1. **Construccion del quadtree** (Barnes-Hut): se calcula el bounding box de las posiciones actuales, se construye un quadtree flat en `quad_pool` (sin allocaciones por nodo). Cada celda acumula centro de masa y masa total. +2. **Repulsion**: para cada nodo se recorre el quadtree. Si el cociente `cell_size / distance < theta`, la celda se trata como una sola masa puntual (multipolo de orden 0). Si no, se desciende a los hijos. Con `theta=0` es O(n²) exacto; con `theta=0.5` es O(n log n). +3. **Atraccion**: para cada arista `(s, t)`, fuerza de Hooke `F = k * dist * weight` en la direccion del arco. +4. **Gravedad**: fuerza proporcional a la distancia al origen, evita que el grafo derive fuera de pantalla. +5. **Integracion**: `v = v * damping + F`, `pos += v`, con clamping de velocidad. +6. Nodos con `pinned = true` no se mueven en ningun paso. + +## Funciones auxiliares + +```cpp +// Randomizar posiciones para empezar la simulacion +graph_force_layout_reset(graph, 200.0f); + +// Layout circular instantaneo (sin iteracion) +graph_layout_circular(graph, 150.0f); + +// Layout en grid instantaneo +graph_layout_grid(graph, 25.0f); +``` + +## Ejemplo de uso tipico (loop ImGui) + +```cpp +static ForceLayoutConfig cfg; +static bool running = true; + +if (running) { + float energy = graph_force_layout_step(my_graph, cfg); + if (energy < 0.01f) running = false; // convergido +} +``` + +## Notas de implementacion + +- El quadtree usa un pool estatico de `1 << 20` (~1M) celdas. Para grafos de >500K nodos + se recomienda reducir `MAX_QUAD_NODES` o aumentarlo segun memoria disponible. +- La pila de traversal en `quad_force` es tambien estatica (`static int stack[]`); no es + thread-safe si se llama desde multiples hilos simultaneamente. +- `graph_force_layout_reset` usa `rand()`. Para reproducibilidad llama `srand(seed)` antes. +- Los buffers de fuerza (`fx_buf`, `fy_buf`) se realocan una sola vez cuando el conteo de + nodos supera la capacidad previa; en el uso normal (tamano fijo) no hay allocaciones + por frame. diff --git a/cpp/functions/viz/graph_renderer.cpp b/cpp/functions/viz/graph_renderer.cpp new file mode 100644 index 00000000..14a1a3e3 --- /dev/null +++ b/cpp/functions/viz/graph_renderer.cpp @@ -0,0 +1,446 @@ +#include "viz/graph_renderer.h" +#include "viz/graph_types.h" + +#define GL_GLEXT_PROTOTYPES +#include +#include + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Community palette (ABGR packed, 10 colors) +// --------------------------------------------------------------------------- +static const uint32_t k_palette[10] = { + 0xFF4CAF50, // green + 0xFFF44336, // red + 0xFF2196F3, // blue + 0xFFFF9800, // orange + 0xFF9C27B0, // purple + 0xFF00BCD4, // cyan + 0xFFFFEB3B, // yellow + 0xFFE91E63, // pink + 0xFF795548, // brown + 0xFF607D8B // blue-grey +}; + +// --------------------------------------------------------------------------- +// Internal struct +// --------------------------------------------------------------------------- +struct GraphRenderer { + unsigned int fbo; + unsigned int texture; + unsigned int rbo; // depth/stencil renderbuffer + int width, height; + + // Node rendering (instanced quads) + unsigned int node_vao, node_quad_vbo, node_instance_vbo; + unsigned int node_shader; + + // Edge rendering (lines) + unsigned int edge_vao, edge_vbo; + unsigned int edge_shader; + + GraphRendererConfig config; +}; + +// --------------------------------------------------------------------------- +// Shader sources +// --------------------------------------------------------------------------- + +// Node vertex shader — instanced unit quad +static const char* k_node_vert = R"( +#version 330 core +// Quad corners [-0.5, 0.5] +layout(location = 0) in vec2 a_quad; + +// Per-instance: world position, size, RGBA color +layout(location = 1) in vec2 a_pos; +layout(location = 2) in float a_size; +layout(location = 3) in vec4 a_color; + +out vec2 v_uv; +out vec4 v_color; + +uniform vec2 u_viewport; // (width, height) in pixels +uniform float u_scale; // cam_zoom +uniform vec2 u_translate; // (tx, ty) in pixels + +void main() { + // World -> screen (pixels) + vec2 screen = a_pos * u_scale + u_translate; + // Expand quad by node radius (size = diameter) + screen += a_quad * a_size * u_scale; + // Screen -> NDC + vec2 ndc = (screen / u_viewport) * 2.0 - 1.0; + ndc.y = -ndc.y; // flip Y (screen Y grows downward) + gl_Position = vec4(ndc, 0.0, 1.0); + v_uv = a_quad + 0.5; // [0,1] + v_color = a_color; +} +)"; + +// Node fragment shader — SDF circle with outline +static const char* k_node_frag = R"( +#version 330 core +in vec2 v_uv; +in vec4 v_color; + +out vec4 frag_color; + +uniform float u_outline_px; // outline width in uv units +uniform float u_node_px; // node diameter in pixels (= size * zoom) + +void main() { + // SDF circle centered at (0.5, 0.5) in uv space + float dist = length(v_uv - 0.5); + float r = 0.5; + + // Anti-alias edge (in uv units; 1px ~ 1/u_node_px in uv space) + float fwidth_uv = 1.5 / max(u_node_px, 1.0); + + float alpha = 1.0 - smoothstep(r - fwidth_uv, r, dist); + if (alpha < 0.001) discard; + + // Outline ring + float outline_uv = u_outline_px / max(u_node_px, 1.0); + float outline = smoothstep(r - outline_uv - fwidth_uv, r - outline_uv, dist); + + vec3 fill = v_color.rgb; + vec3 outline_col = mix(fill, vec3(1.0), 0.6); // lighter outline + vec3 color = mix(fill, outline_col, outline); + + frag_color = vec4(color, v_color.a * alpha); +} +)"; + +// Edge vertex shader +static const char* k_edge_vert = R"( +#version 330 core +layout(location = 0) in vec2 a_pos; +layout(location = 1) in vec4 a_color; + +out vec4 v_color; + +uniform vec2 u_viewport; +uniform float u_scale; +uniform vec2 u_translate; + +void main() { + vec2 screen = a_pos * u_scale + u_translate; + vec2 ndc = (screen / u_viewport) * 2.0 - 1.0; + ndc.y = -ndc.y; + gl_Position = vec4(ndc, 0.0, 1.0); + v_color = a_color; +} +)"; + +// Edge fragment shader +static const char* k_edge_frag = R"( +#version 330 core +in vec4 v_color; +out vec4 frag_color; + +void main() { + frag_color = v_color; +} +)"; + +// --------------------------------------------------------------------------- +// Shader helpers +// --------------------------------------------------------------------------- +static unsigned int compile_shader(GLenum type, const char* src) { + unsigned int s = glCreateShader(type); + glShaderSource(s, 1, &src, nullptr); + glCompileShader(s); + int ok; + glGetShaderiv(s, GL_COMPILE_STATUS, &ok); + if (!ok) { + char buf[512]; + glGetShaderInfoLog(s, sizeof(buf), nullptr, buf); + fprintf(stderr, "[graph_renderer] shader compile error: %s\n", buf); + } + return s; +} + +static unsigned int link_program(const char* vert_src, const char* frag_src) { + unsigned int vs = compile_shader(GL_VERTEX_SHADER, vert_src); + unsigned int fs = compile_shader(GL_FRAGMENT_SHADER, frag_src); + unsigned int prog = glCreateProgram(); + glAttachShader(prog, vs); + glAttachShader(prog, fs); + glLinkProgram(prog); + int ok; + glGetProgramiv(prog, GL_LINK_STATUS, &ok); + if (!ok) { + char buf[512]; + glGetProgramInfoLog(prog, sizeof(buf), nullptr, buf); + fprintf(stderr, "[graph_renderer] program link error: %s\n", buf); + } + glDeleteShader(vs); + glDeleteShader(fs); + return prog; +} + +// --------------------------------------------------------------------------- +// FBO helpers +// --------------------------------------------------------------------------- +static void create_fbo(GraphRenderer* r) { + // Texture + glGenTextures(1, &r->texture); + glBindTexture(GL_TEXTURE_2D, r->texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, r->width, r->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glBindTexture(GL_TEXTURE_2D, 0); + + // Depth renderbuffer + glGenRenderbuffers(1, &r->rbo); + glBindRenderbuffer(GL_RENDERBUFFER, r->rbo); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, r->width, r->height); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + // FBO + glGenFramebuffers(1, &r->fbo); + glBindFramebuffer(GL_FRAMEBUFFER, r->fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, r->texture, 0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, r->rbo); + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +static void destroy_fbo(GraphRenderer* r) { + glDeleteFramebuffers(1, &r->fbo); + glDeleteTextures(1, &r->texture); + glDeleteRenderbuffers(1, &r->rbo); + r->fbo = r->texture = r->rbo = 0; +} + +// --------------------------------------------------------------------------- +// Helper: unpack ABGR uint32 to float RGBA +// --------------------------------------------------------------------------- +static inline void abgr_to_rgba(uint32_t abgr, float& r, float& g, float& b, float& a) { + // ABGR layout: bits 31-24 = A, 23-16 = B, 15-8 = G, 7-0 = R + a = ((abgr >> 24) & 0xFF) / 255.0f; + b = ((abgr >> 16) & 0xFF) / 255.0f; + g = ((abgr >> 8) & 0xFF) / 255.0f; + r = ((abgr ) & 0xFF) / 255.0f; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config) { + GraphRenderer* r = new GraphRenderer(); + r->width = width; + r->height = height; + r->config = config; + + // --- FBO --- + create_fbo(r); + + // --- Node VAO --- + // Unit quad: 4 vertices, each (x, y) in [-0.5, 0.5] + static const float quad_verts[8] = { + -0.5f, -0.5f, + 0.5f, -0.5f, + -0.5f, 0.5f, + 0.5f, 0.5f, + }; + + glGenVertexArrays(1, &r->node_vao); + glBindVertexArray(r->node_vao); + + // Quad VBO (location 0) + glGenBuffers(1, &r->node_quad_vbo); + glBindBuffer(GL_ARRAY_BUFFER, r->node_quad_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); + + // Instance VBO (location 1,2,3 — position, size, color) + glGenBuffers(1, &r->node_instance_vbo); + glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo); + // layout: x, y, size, r, g, b, a — 7 floats per instance + glEnableVertexAttribArray(1); // pos + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)0); + glVertexAttribDivisor(1, 1); + glEnableVertexAttribArray(2); // size + glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(2 * sizeof(float))); + glVertexAttribDivisor(2, 1); + glEnableVertexAttribArray(3); // color + glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(3 * sizeof(float))); + glVertexAttribDivisor(3, 1); + + glBindVertexArray(0); + + // --- Edge VAO --- + // Each edge: 2 vertices x (x, y, r, g, b, a) = 2 * 6 floats + glGenVertexArrays(1, &r->edge_vao); + glBindVertexArray(r->edge_vao); + + glGenBuffers(1, &r->edge_vbo); + glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo); + glEnableVertexAttribArray(0); // pos + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); + glEnableVertexAttribArray(1); // color + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(2 * sizeof(float))); + + glBindVertexArray(0); + + // --- Shaders --- + r->node_shader = link_program(k_node_vert, k_node_frag); + r->edge_shader = link_program(k_edge_vert, k_edge_frag); + + return r; +} + +void graph_renderer_destroy(GraphRenderer* r) { + if (!r) return; + destroy_fbo(r); + glDeleteVertexArrays(1, &r->node_vao); + glDeleteBuffers(1, &r->node_quad_vbo); + glDeleteBuffers(1, &r->node_instance_vbo); + glDeleteVertexArrays(1, &r->edge_vao); + glDeleteBuffers(1, &r->edge_vbo); + glDeleteProgram(r->node_shader); + glDeleteProgram(r->edge_shader); + delete r; +} + +void graph_renderer_resize(GraphRenderer* r, int width, int height) { + if (!r) return; + if (r->width == width && r->height == height) return; + r->width = width; + r->height = height; + destroy_fbo(r); + create_fbo(r); +} + +unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph, + float cam_x, float cam_y, float cam_zoom) { + if (!r) return 0; + + // --- Save GL state --- + GLint prev_fbo; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo); + GLint prev_viewport[4]; + glGetIntegerv(GL_VIEWPORT, prev_viewport); + + // --- Bind FBO --- + glBindFramebuffer(GL_FRAMEBUFFER, r->fbo); + glViewport(0, 0, r->width, r->height); + + // Clear with bg_color (ABGR) + float bg_a, bg_b, bg_g, bg_cr; + abgr_to_rgba(r->config.bg_color, bg_cr, bg_g, bg_b, bg_a); + glClearColor(bg_cr, bg_g, bg_b, bg_a); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Enable blending for anti-aliasing and transparency + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // View transform: world -> screen pixels + // tx = -cam_x * scale + width/2 + // ty = -cam_y * scale + height/2 + float scale = cam_zoom; + float tx = -cam_x * scale + (float)r->width * 0.5f; + float ty = -cam_y * scale + (float)r->height * 0.5f; + + // ---------------------------------------------------------------- + // Draw edges + // ---------------------------------------------------------------- + if (graph.edge_count > 0 && graph.edges && graph.nodes) { + // Pack: 2 vertices per edge, each vertex = (x, y, r, g, b, a) = 6 floats + const int floats_per_edge = 2 * 6; + float* edge_buf = (float*)malloc((size_t)graph.edge_count * floats_per_edge * sizeof(float)); + int vi = 0; + for (int i = 0; i < graph.edge_count; ++i) { + const GraphEdge& e = graph.edges[i]; + uint32_t ecol = e.color != 0 ? e.color : 0xFF888888u; // default gray + float er, eg, eb, ea; + abgr_to_rgba(ecol, er, eg, eb, ea); + ea *= r->config.edge_alpha; + + if (e.source < (uint32_t)graph.node_count && e.target < (uint32_t)graph.node_count) { + const GraphNode& ns = graph.nodes[e.source]; + const GraphNode& nt = graph.nodes[e.target]; + + // Source vertex + edge_buf[vi++] = ns.x; edge_buf[vi++] = ns.y; + edge_buf[vi++] = er; edge_buf[vi++] = eg; + edge_buf[vi++] = eb; edge_buf[vi++] = ea; + // Target vertex + edge_buf[vi++] = nt.x; edge_buf[vi++] = nt.y; + edge_buf[vi++] = er; edge_buf[vi++] = eg; + edge_buf[vi++] = eb; edge_buf[vi++] = ea; + } + } + + glUseProgram(r->edge_shader); + glUniform2f(glGetUniformLocation(r->edge_shader, "u_viewport"), (float)r->width, (float)r->height); + glUniform1f(glGetUniformLocation(r->edge_shader, "u_scale"), scale); + glUniform2f(glGetUniformLocation(r->edge_shader, "u_translate"), tx, ty); + + glLineWidth(r->config.edge_width); + + glBindVertexArray(r->edge_vao); + glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo); + glBufferData(GL_ARRAY_BUFFER, vi * (int)sizeof(float), edge_buf, GL_DYNAMIC_DRAW); + glDrawArrays(GL_LINES, 0, vi / 6); + glBindVertexArray(0); + + free(edge_buf); + } + + // ---------------------------------------------------------------- + // Draw nodes (instanced quads) + // ---------------------------------------------------------------- + if (graph.node_count > 0 && graph.nodes) { + // Pack: 7 floats per node: x, y, size, r, g, b, a + float* node_buf = (float*)malloc((size_t)graph.node_count * 7 * sizeof(float)); + for (int i = 0; i < graph.node_count; ++i) { + const GraphNode& n = graph.nodes[i]; + uint32_t ncol = n.color != 0 ? n.color : k_palette[n.community % 10]; + float nr, ng, nb, na; + abgr_to_rgba(ncol, nr, ng, nb, na); + + float sz = n.size > 0.0f ? n.size : 4.0f; + float* p = node_buf + i * 7; + p[0] = n.x; p[1] = n.y; p[2] = sz; + p[3] = nr; p[4] = ng; p[5] = nb; p[6] = na; + } + + glUseProgram(r->node_shader); + glUniform2f(glGetUniformLocation(r->node_shader, "u_viewport"), (float)r->width, (float)r->height); + glUniform1f(glGetUniformLocation(r->node_shader, "u_scale"), scale); + glUniform2f(glGetUniformLocation(r->node_shader, "u_translate"), tx, ty); + glUniform1f(glGetUniformLocation(r->node_shader, "u_outline_px"), r->config.node_outline); + + glBindVertexArray(r->node_vao); + glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo); + glBufferData(GL_ARRAY_BUFFER, graph.node_count * 7 * (int)sizeof(float), node_buf, GL_DYNAMIC_DRAW); + + // Draw 4 vertices (triangle strip quad) x node_count instances + // Pass per-instance node_px uniform via the average size (approximation) + // For exact per-node pixel size we'd need a texture or another approach; + // use a uniform average for AA quality — good enough for most graphs. + float avg_px = 8.0f * scale; // rough estimate + glUniform1f(glGetUniformLocation(r->node_shader, "u_node_px"), avg_px); + + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, graph.node_count); + glBindVertexArray(0); + + free(node_buf); + } + + // --- Restore GL state --- + glDisable(GL_BLEND); + glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo); + glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]); + + return r->texture; +} diff --git a/cpp/functions/viz/graph_renderer.h b/cpp/functions/viz/graph_renderer.h new file mode 100644 index 00000000..eccf17b7 --- /dev/null +++ b/cpp/functions/viz/graph_renderer.h @@ -0,0 +1,28 @@ +#pragma once +#include + +struct GraphData; // forward declare + +struct GraphRendererConfig { + float node_outline = 1.5f; // outline width in pixels + float edge_width = 1.0f; // edge line width + float edge_alpha = 0.4f; // edge transparency + uint32_t bg_color = 0xFF1A1A1E; // ABGR background + bool edge_fade_alpha = true; // fade edge alpha by distance to camera + // Default palette for communities (when node.color == 0) + // 10 distinct colors, ABGR packed +}; + +struct GraphRenderer; + +// Lifecycle +GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config = {}); +void graph_renderer_destroy(GraphRenderer* r); +void graph_renderer_resize(GraphRenderer* r, int width, int height); + +// Render graph to internal FBO. +// cam_x, cam_y: camera center in graph space +// cam_zoom: zoom level (1.0 = 1:1 pixel mapping) +// Returns OpenGL texture ID suitable for ImGui::Image(). +unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph, + float cam_x, float cam_y, float cam_zoom); diff --git a/cpp/functions/viz/graph_renderer.md b/cpp/functions/viz/graph_renderer.md new file mode 100644 index 00000000..f6569dc1 --- /dev/null +++ b/cpp/functions/viz/graph_renderer.md @@ -0,0 +1,87 @@ +--- +name: graph_renderer +kind: function +lang: cpp +domain: viz +version: "1.0.0" +purity: impure +signature: "GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config)" +description: "Renderer GPU de grafos con instanced rendering a FBO, compatible con ImGui::Image para visualizacion de grafos grandes" +tags: [graph, renderer, opengl, gpu, instanced, fbo, visualization] +uses_functions: [] +uses_types: ["GraphData_cpp_viz"] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/graph_renderer.cpp" +framework: imgui +params: + - name: width + desc: "Ancho del framebuffer en pixels" + - name: height + desc: "Alto del framebuffer en pixels" + - name: config + desc: "Configuracion visual: outline width, edge width, edge alpha, color de fondo, fade de aristas por distancia a camara" +output: "Handle opaco al renderer. Usar graph_renderer_draw() para obtener texture ID de OpenGL, pasable directamente a ImGui::Image()" +--- + +# graph_renderer + +Renderer GPU de grafos basado en OpenGL 3.3 core profile con instanced rendering. Renderiza nodos y aristas de un `GraphData` a un FBO interno y retorna el texture ID para integracion directa con `ImGui::Image()`. + +## Funciones del API + +```cpp +// Ciclo de vida +GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config = {}); +void graph_renderer_destroy(GraphRenderer* r); +void graph_renderer_resize(GraphRenderer* r, int width, int height); + +// Renderizado +unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph, + float cam_x, float cam_y, float cam_zoom); +``` + +## Ejemplo de uso con ImGui + +```cpp +// Inicializacion (una vez) +GraphRenderer* renderer = graph_renderer_create(800, 600); + +// En el render loop +ImVec2 panel_size = ImGui::GetContentRegionAvail(); +graph_renderer_resize(renderer, (int)panel_size.x, (int)panel_size.y); + +unsigned int tex = graph_renderer_draw(renderer, graph_data, + cam_x, cam_y, cam_zoom); + +ImGui::Image((ImTextureID)(uintptr_t)tex, + panel_size, + ImVec2(0, 1), ImVec2(1, 0)); // flip Y para OpenGL + +// Destruccion +graph_renderer_destroy(renderer); +``` + +## Notas de implementacion + +**Renderizado de nodos:** Instanced rendering con un quad unitario [-0.5, 0.5] expandido por el tamano del nodo. El fragment shader aplica un SDF circular con anti-aliasing via `smoothstep` y un anillo de outline. + +**Renderizado de aristas:** `GL_LINES` con datos de posicion y color empaquetados por arista. El ancho se controla con `GraphRendererConfig::edge_width`. + +**Transformacion de camara:** +``` +tx = -cam_x * zoom + width/2 +ty = -cam_y * zoom + height/2 +ndc = (screen / viewport) * 2 - 1 +``` + +**Paleta de comunidades:** 10 colores ABGR usados cuando `node.color == 0`, seleccionados por `node.community % 10`. + +**Estado GL:** Guarda y restaura `GL_FRAMEBUFFER_BINDING` y `GL_VIEWPORT` para ser compatible con el render loop de ImGui sin efectos secundarios. + +**Includes GL:** Usa `#define GL_GLEXT_PROTOTYPES` + `` + ``. Si el proyecto carga funciones GL via glad/gl3w, reemplazar estos includes por el loader correspondiente. diff --git a/cpp/functions/viz/graph_types.cpp b/cpp/functions/viz/graph_types.cpp new file mode 100644 index 00000000..4a3c7e9b --- /dev/null +++ b/cpp/functions/viz/graph_types.cpp @@ -0,0 +1,24 @@ +#include "graph_types.h" +#include + +void GraphData::update_bounds() { + if (node_count == 0) { + min_x = min_y = max_x = max_y = 0.0f; + return; + } + min_x = max_x = nodes[0].x; + min_y = max_y = nodes[0].y; + for (int i = 1; i < node_count; ++i) { + if (nodes[i].x < min_x) min_x = nodes[i].x; + if (nodes[i].x > max_x) max_x = nodes[i].x; + if (nodes[i].y < min_y) min_y = nodes[i].y; + if (nodes[i].y > max_y) max_y = nodes[i].y; + } +} + +int GraphData::find_node(uint32_t id) const { + for (int i = 0; i < node_count; ++i) { + if (nodes[i].id == id) return i; + } + return -1; +} diff --git a/cpp/functions/viz/graph_types.h b/cpp/functions/viz/graph_types.h new file mode 100644 index 00000000..672ff1cf --- /dev/null +++ b/cpp/functions/viz/graph_types.h @@ -0,0 +1,50 @@ +#pragma once +#include + +// --- Graph node --- +struct GraphNode { + uint32_t id; + float x, y; // position in layout space + float vx, vy; // velocity (used by force layout) + float size; // visual radius (default 4.0) + uint32_t color; // ABGR packed (0 = use default palette) + const char* label; // optional display label (nullptr = none) + uint32_t community; // group/cluster ID (for auto-coloring) + float value; // arbitrary metric (for sizing) + bool pinned; // if true, force layout won't move this node +}; + +// --- Graph edge --- +struct GraphEdge { + uint32_t source; // index into GraphData::nodes + uint32_t target; // index into GraphData::nodes + float weight; // edge weight (affects attraction force) + uint32_t color; // ABGR packed (0 = default gray) +}; + +// --- Graph container --- +struct GraphData { + GraphNode* nodes; + int node_count; + GraphEdge* edges; + int edge_count; + + // Bounding box (updated by layout) + float min_x, min_y, max_x, max_y; + + // Recompute bounding box from node positions + void update_bounds(); + + // Find node index by id. Returns -1 if not found. + int find_node(uint32_t id) const; +}; + +// --- Helper: create a default node --- +inline GraphNode graph_node(uint32_t id, float x = 0, float y = 0) { + return {id, x, y, 0, 0, 4.0f, 0, nullptr, 0, 0, false}; +} + +// --- Helper: create an edge --- +inline GraphEdge graph_edge(uint32_t source, uint32_t target, float weight = 1.0f) { + return {source, target, weight, 0}; +} diff --git a/cpp/functions/viz/graph_viewport.cpp b/cpp/functions/viz/graph_viewport.cpp new file mode 100644 index 00000000..b754f55d --- /dev/null +++ b/cpp/functions/viz/graph_viewport.cpp @@ -0,0 +1,327 @@ +#include "viz/graph_viewport.h" +#include "viz/graph_types.h" +#include "viz/graph_renderer.h" +#include "viz/graph_force_layout.h" +#include "core/graph_spatial_hash.h" +#include "imgui.h" + +#include // snprintf +#include // memset +#include + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +static void viewport_to_graph(float vx, float vy, + float widget_x, float widget_y, + float widget_w, float widget_h, + float cam_x, float cam_y, float zoom, + float& gx, float& gy) +{ + gx = (vx - widget_x - widget_w * 0.5f) / zoom + cam_x; + gy = (vy - widget_y - widget_h * 0.5f) / zoom + cam_y; +} + +// --------------------------------------------------------------------------- +// graph_viewport_fit +// --------------------------------------------------------------------------- + +void graph_viewport_fit(GraphData& graph, GraphViewportState& state) +{ + graph.update_bounds(); + if (graph.node_count == 0) { + state.cam_x = 0.0f; + state.cam_y = 0.0f; + state.zoom = 1.0f; + return; + } + + float cx = (graph.min_x + graph.max_x) * 0.5f; + float cy = (graph.min_y + graph.max_y) * 0.5f; + state.cam_x = cx; + state.cam_y = cy; + + float span_x = graph.max_x - graph.min_x; + float span_y = graph.max_y - graph.min_y; + float span = (span_x > span_y ? span_x : span_y); + + // Use render dimensions if available; fall back to a safe default. + float view_px = (state.render_w > 0 ? (float)state.render_w : 600.0f); + float view_py = (state.render_h > 0 ? (float)state.render_h : 400.0f); + float view_min = (view_px < view_py ? view_px : view_py); + + if (span > 0.0f) { + state.zoom = (view_min * 0.9f) / span; + } else { + state.zoom = 1.0f; + } + + // Clamp to allowed range + if (state.zoom < state.zoom_min) state.zoom = state.zoom_min; + if (state.zoom > state.zoom_max) state.zoom = state.zoom_max; +} + +// --------------------------------------------------------------------------- +// graph_viewport_destroy +// --------------------------------------------------------------------------- + +void graph_viewport_destroy(GraphViewportState& state) +{ + if (state.renderer) { + graph_renderer_destroy(state.renderer); + state.renderer = nullptr; + } + if (state.spatial) { + delete state.spatial; + state.spatial = nullptr; + } + state.initialized = false; +} + +// --------------------------------------------------------------------------- +// graph_viewport — main widget +// --------------------------------------------------------------------------- + +bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, + ImVec2 size) +{ + bool interacted = false; + + // Resolve size + ImVec2 avail = ImGui::GetContentRegionAvail(); + float w = (size.x > 0.0f) ? size.x : avail.x; + float h = (size.y > 0.0f) ? size.y : avail.y; + if (w < 1.0f) w = 1.0f; + if (h < 1.0f) h = 1.0f; + + int iw = (int)w, ih = (int)h; + + // ------------------------------------------------------------------- + // 1. Lazy init + // ------------------------------------------------------------------- + if (!state.initialized) { + state.renderer = graph_renderer_create(iw, ih); + state.spatial = new SpatialHash(20.0f, 4096); + state.render_w = iw; + state.render_h = ih; + state.initialized = true; + graph_viewport_fit(graph, state); + } + + // ------------------------------------------------------------------- + // 2. Resize + // ------------------------------------------------------------------- + if (iw != state.render_w || ih != state.render_h) { + graph_renderer_resize(state.renderer, iw, ih); + state.render_w = iw; + state.render_h = ih; + } + + // ------------------------------------------------------------------- + // 3. Force layout step + // ------------------------------------------------------------------- + if (state.layout_running && graph.node_count > 0) { + state.layout_energy = graph_force_layout_step(graph); + if (state.layout_energy < 0.01f) { + state.layout_running = false; + } + } + + // ------------------------------------------------------------------- + // 4. Build spatial hash + // ------------------------------------------------------------------- + if (graph.node_count > 0) { + static std::vector xs_buf, ys_buf, sz_buf; + xs_buf.resize(graph.node_count); + ys_buf.resize(graph.node_count); + sz_buf.resize(graph.node_count); + for (int i = 0; i < graph.node_count; ++i) { + xs_buf[i] = graph.nodes[i].x; + ys_buf[i] = graph.nodes[i].y; + sz_buf[i] = graph.nodes[i].size; + } + state.spatial->build(xs_buf.data(), ys_buf.data(), sz_buf.data(), graph.node_count); + } + + // ------------------------------------------------------------------- + // 5. Invisible button to capture input + // ------------------------------------------------------------------- + ImGui::PushID(id); + + ImVec2 widget_pos = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("canvas", ImVec2(w, h), + ImGuiButtonFlags_MouseButtonLeft | + ImGuiButtonFlags_MouseButtonMiddle| + ImGuiButtonFlags_MouseButtonRight); + + bool hovered = ImGui::IsItemHovered(); + bool lm_down = ImGui::IsMouseDown(ImGuiMouseButton_Left); + bool lm_click = ImGui::IsMouseClicked(ImGuiMouseButton_Left); + bool mm_down = ImGui::IsMouseDown(ImGuiMouseButton_Middle); + bool rm_down = ImGui::IsMouseDown(ImGuiMouseButton_Right); + + ImVec2 mouse_pos = ImGui::GetMousePos(); + float mx = mouse_pos.x, my = mouse_pos.y; + + // Convert mouse to graph space + float gx_mouse, gy_mouse; + viewport_to_graph(mx, my, + widget_pos.x, widget_pos.y, w, h, + state.cam_x, state.cam_y, state.zoom, + gx_mouse, gy_mouse); + + // ------------------------------------------------------------------- + // 5a. Pan (middle or right mouse drag) + // ------------------------------------------------------------------- + if (hovered && (mm_down || rm_down)) { + ImVec2 delta = ImGui::GetIO().MouseDelta; + if (delta.x != 0.0f || delta.y != 0.0f) { + state.cam_x -= delta.x / state.zoom; + state.cam_y -= delta.y / state.zoom; + interacted = true; + } + } + + // ------------------------------------------------------------------- + // 5b. Zoom (scroll wheel) + // ------------------------------------------------------------------- + if (hovered) { + float wheel = ImGui::GetIO().MouseWheel; + if (wheel != 0.0f) { + float old_zoom = state.zoom; + float new_zoom = old_zoom * (1.0f + wheel * 0.1f); + if (new_zoom < state.zoom_min) new_zoom = state.zoom_min; + if (new_zoom > state.zoom_max) new_zoom = state.zoom_max; + + // Zoom toward cursor: keep gx_mouse/gy_mouse fixed in graph space + float rel_x = (mx - widget_pos.x - w * 0.5f); + float rel_y = (my - widget_pos.y - h * 0.5f); + state.cam_x += rel_x / old_zoom - rel_x / new_zoom; + state.cam_y += rel_y / old_zoom - rel_y / new_zoom; + state.zoom = new_zoom; + interacted = true; + } + } + + // ------------------------------------------------------------------- + // 5c. Hover — query nearest node + // ------------------------------------------------------------------- + state.hovered_node = -1; + if (hovered && graph.node_count > 0) { + float hit_radius = 10.0f / state.zoom; + int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, hit_radius); + if (nearest >= 0) { + state.hovered_node = nearest; + interacted = true; + } + } + + // ------------------------------------------------------------------- + // 5d. Node drag (left mouse down on a node) + // ------------------------------------------------------------------- + if (hovered && lm_down) { + if (state.drag_node == -1 && state.hovered_node >= 0) { + state.drag_node = state.hovered_node; + state.is_dragging = true; + } + } else { + // Release drag + if (state.drag_node >= 0 && state.drag_node < graph.node_count) { + graph.nodes[state.drag_node].pinned = false; + } + state.drag_node = -1; + state.is_dragging = false; + } + + if (state.drag_node >= 0 && state.drag_node < graph.node_count) { + GraphNode& n = graph.nodes[state.drag_node]; + n.x = gx_mouse; + n.y = gy_mouse; + n.vx = 0.0f; + n.vy = 0.0f; + n.pinned = true; + interacted = true; + } + + // ------------------------------------------------------------------- + // 5e. Click — select node + // ------------------------------------------------------------------- + if (hovered && lm_click && state.drag_node == -1) { + state.selected_node = state.hovered_node; + interacted = true; + } + + // ------------------------------------------------------------------- + // 5f. Keyboard shortcuts (only when widget is active/hovered) + // ------------------------------------------------------------------- + if (hovered) { + if (ImGui::IsKeyPressed(ImGuiKey_Space)) { + state.layout_running = !state.layout_running; + } + if (ImGui::IsKeyPressed(ImGuiKey_F)) { + graph_viewport_fit(graph, state); + interacted = true; + } + } + + // ------------------------------------------------------------------- + // 6. Render to GPU texture + // ------------------------------------------------------------------- + unsigned int tex_id = graph_renderer_draw(state.renderer, graph, + state.cam_x, state.cam_y, + state.zoom); + + // ------------------------------------------------------------------- + // 7. Display texture (flip UV for OpenGL FBO convention) + // ------------------------------------------------------------------- + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddImage( + (ImTextureID)(intptr_t)tex_id, + widget_pos, + ImVec2(widget_pos.x + w, widget_pos.y + h), + ImVec2(0.0f, 1.0f), // UV top-left (flipped Y) + ImVec2(1.0f, 0.0f) // UV bottom-right + ); + + // ------------------------------------------------------------------- + // 8. Tooltip on hovered node + // ------------------------------------------------------------------- + if (state.hovered_node >= 0 && state.hovered_node < graph.node_count) { + const GraphNode& n = graph.nodes[state.hovered_node]; + + // Count degree + int degree = 0; + for (int i = 0; i < graph.edge_count; ++i) { + if ((int)graph.edges[i].source == state.hovered_node || + (int)graph.edges[i].target == state.hovered_node) { + ++degree; + } + } + + ImGui::BeginTooltip(); + if (n.label) ImGui::TextUnformatted(n.label); + ImGui::Text("ID: %u", n.id); + ImGui::Text("Community: %u", n.community); + ImGui::Text("Degree: %d", degree); + ImGui::Text("Value: %.3f", n.value); + ImGui::EndTooltip(); + } + + // ------------------------------------------------------------------- + // 9. Status bar overlay + // ------------------------------------------------------------------- + { + char status[128]; + snprintf(status, sizeof(status), + "Nodes: %d | Edges: %d | Zoom: %.2fx | Energy: %.4f | [Space] layout [F] fit", + graph.node_count, graph.edge_count, + state.zoom, state.layout_energy); + + ImVec2 text_pos = ImVec2(widget_pos.x + 6.0f, widget_pos.y + h - 18.0f); + draw_list->AddText(text_pos, IM_COL32(180, 180, 180, 200), status); + } + + ImGui::PopID(); + return interacted; +} diff --git a/cpp/functions/viz/graph_viewport.h b/cpp/functions/viz/graph_viewport.h new file mode 100644 index 00000000..5ac67ff3 --- /dev/null +++ b/cpp/functions/viz/graph_viewport.h @@ -0,0 +1,50 @@ +#pragma once +#include "imgui.h" + +struct GraphData; +struct GraphRenderer; +struct SpatialHash; + +// Persistent state for graph_viewport widget. Create one per viewport and keep +// alive across frames. +struct GraphViewportState { + // Camera + float cam_x = 0.0f, cam_y = 0.0f; + float zoom = 1.0f; + float zoom_min = 0.01f, zoom_max = 50.0f; + + // Interaction result (read after calling graph_viewport each frame) + int hovered_node = -1; // node index under cursor, -1 if none + int selected_node = -1; // last clicked node index, -1 if none + bool is_dragging = false; + + // Layout + bool layout_running = true; // animate force layout each frame + float layout_energy = 0.0f; // kinetic energy from last step + + // Internal — managed by graph_viewport / graph_viewport_destroy + GraphRenderer* renderer = nullptr; + SpatialHash* spatial = nullptr; + bool initialized = false; + + // Widget pixel dimensions tracked for resize detection + int render_w = 0, render_h = 0; + + // Node being dragged (-1 = none) + int drag_node = -1; +}; + +// Main viewport widget. Call every ImGui frame. +// id: unique ImGui widget identifier +// graph: mutable graph data (node positions updated on drag) +// state: persistent state (camera, selection, GPU renderer); must outlive frames +// size: widget size in pixels — ImVec2(0,0) uses all available space +// Returns true if any user interaction occurred (hover, click, drag, zoom). +bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, + ImVec2 size = ImVec2(0.0f, 0.0f)); + +// Release GPU resources. Call once when done with the viewport. +void graph_viewport_destroy(GraphViewportState& state); + +// Fit camera to current graph bounds with 10% padding. +void graph_viewport_fit(GraphData& graph, GraphViewportState& state); diff --git a/cpp/functions/viz/graph_viewport.md b/cpp/functions/viz/graph_viewport.md new file mode 100644 index 00000000..d3a0c611 --- /dev/null +++ b/cpp/functions/viz/graph_viewport.md @@ -0,0 +1,119 @@ +--- +name: graph_viewport +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: impure +signature: "bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, ImVec2 size)" +description: "Widget ImGui completo para visualizacion interactiva de grafos con pan, zoom, hover, seleccion y layout en vivo" +tags: [graph, viewport, imgui, interactive, pan, zoom, dashboard] +uses_functions: ["graph_renderer_cpp_viz", "graph_force_layout_cpp_viz", "graph_spatial_hash_cpp_core"] +uses_types: ["GraphData_cpp_viz"] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/graph_viewport.cpp" +framework: imgui +props: + - name: id + type: "const char*" + required: true + description: "Identificador unico del widget ImGui" + - name: graph + type: "GraphData&" + required: true + description: "Referencia al grafo (lectura de datos, escritura de posiciones al drag)" + - name: state + type: "GraphViewportState&" + required: true + description: "Estado persistente del viewport (camera, seleccion, renderer). Debe vivir mas que los frames." + - name: size + type: "ImVec2" + required: false + description: "Tamanio del widget en pixeles. ImVec2(0,0) usa todo el espacio disponible." +emits: [] +has_state: true +params: + - name: id + desc: "Identificador unico del widget ImGui. Debe ser estable entre frames." + - name: graph + desc: "Grafo a visualizar. Las posiciones de nodos se modifican al arrastrar." + - name: state + desc: "Estado persistente: camara (cam_x, cam_y, zoom), nodo seleccionado/hovereado, renderer GPU, spatial hash. Alojado por el caller." + - name: size + desc: "Tamanio del widget en pixeles. (0,0) ocupa todo el espacio disponible en la ventana ImGui." +output: "true si hubo alguna interaccion del usuario en el frame actual (hover, click, drag, zoom, teclado)" +--- + +# graph_viewport + +Widget ImGui self-contained para visualizar grafos interactivos. Integra rendering GPU, force-directed layout y hit-testing espacial en una sola llamada por frame. + +## Uso basico + +```cpp +// Declarar estado persistente (fuera del loop de render) +GraphViewportState vp_state; + +// En el loop de render (dentro de una ventana ImGui): +if (graph_viewport("mi_grafo", my_graph, vp_state)) { + // hubo interaccion este frame + if (vp_state.selected_node >= 0) { + auto& n = my_graph.nodes[vp_state.selected_node]; + // mostrar panel de detalle de n + } +} + +// Al terminar: +graph_viewport_destroy(vp_state); +``` + +## Estado de camara + +La camara usa coordenadas del espacio del grafo: + +- `cam_x`, `cam_y`: centro de la camara en espacio del grafo +- `zoom`: pixeles por unidad de grafo + +`graph_viewport_fit()` centra y ajusta el zoom para que el grafo quepa con 10% de padding. + +## Controles + +| Accion | Control | +|--------|---------| +| Pan | Boton medio o derecho + arrastrar | +| Zoom | Rueda del raton (hacia el cursor) | +| Seleccionar nodo | Click izquierdo | +| Arrastrar nodo | Click izquierdo sobre nodo | +| Toggle layout | Barra espaciadora | +| Fit camara | F | + +## Force layout + +El layout se ejecuta automaticamente cada frame mientras `state.layout_running == true`. Se detiene solo cuando la energia cinetica cae por debajo de `0.01`. Se puede pausar/reanudar con la barra espaciadora. + +Los nodos arrastrados se marcan como `pinned = true` durante el drag, impidiendo que el force layout los mueva. Al soltar, `pinned` vuelve a `false`. + +## Tooltip + +Al hacer hover sobre un nodo se muestra un tooltip con: label, id numerico, community, degree (aristas conectadas) y value. + +## Status bar + +En la parte inferior del widget aparece: numero de nodos, aristas, zoom actual, energia del layout y recordatorio de atajos de teclado. + +## Inicializacion lazy + +El renderer OpenGL y el spatial hash se crean en el primer frame. La camara se ajusta automaticamente con `graph_viewport_fit` en la inicializacion. + +## Notas de implementacion + +- Usa `ImGui::InvisibleButton` con flags para los tres botones del raton, capturando input sin dibujar ningun boton visible. +- La textura del renderer se muestra con UV volteado en Y (`ImVec2(0,1)` a `ImVec2(1,0)`) para corregir la convencion de coordenadas de OpenGL vs ImGui. +- El spatial hash se reconstruye cada frame desde las posiciones actuales de los nodos, garantizando hit-testing correcto despues de drag o layout. +- El zoom hacia el cursor mantiene el punto del grafo bajo el cursor fijo en pantalla ajustando `cam_x`/`cam_y`. diff --git a/cpp/functions/viz/histogram.cpp b/cpp/functions/viz/histogram.cpp new file mode 100644 index 00000000..3665fb8f --- /dev/null +++ b/cpp/functions/viz/histogram.cpp @@ -0,0 +1,18 @@ +#include "viz/histogram.h" +#include "implot.h" + +void histogram(const char* title, const float* values, int count, int bins) { + if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) { + int b = (bins > 0) ? bins : ImPlotBin_Sturges; + ImPlot::PlotHistogram("##data", values, count, b); + ImPlot::EndPlot(); + } +} + +void histogram(const char* title, const double* values, int count, int bins) { + if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) { + int b = (bins > 0) ? bins : ImPlotBin_Sturges; + ImPlot::PlotHistogram("##data", values, count, b); + ImPlot::EndPlot(); + } +} diff --git a/cpp/functions/viz/histogram.h b/cpp/functions/viz/histogram.h new file mode 100644 index 00000000..1f89270f --- /dev/null +++ b/cpp/functions/viz/histogram.h @@ -0,0 +1,7 @@ +#pragma once + +// Renders a histogram using ImPlot::PlotHistogram. +// Call within an ImGui frame. +// bins == -1: automatic bin count via Sturges' rule. +void histogram(const char* title, const float* values, int count, int bins = -1); +void histogram(const char* title, const double* values, int count, int bins = -1); diff --git a/cpp/functions/viz/histogram.md b/cpp/functions/viz/histogram.md new file mode 100644 index 00000000..3a5b7974 --- /dev/null +++ b/cpp/functions/viz/histogram.md @@ -0,0 +1,42 @@ +--- +name: histogram +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "void histogram(const char* title, const float* values, int count, int bins = -1)" +description: "Renderiza un histograma con bins automaticos o manuales usando ImPlot PlotHistogram dentro de un frame ImGui" +tags: [implot, chart, visualization, gpu, histogram, distribution] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [implot] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/histogram.cpp" +framework: imgui +params: + - name: title + desc: "Titulo del histograma mostrado como cabecera del plot" + - name: values + desc: "Array de valores numericos a distribuir en bins" + - name: count + desc: "Numero de valores en el array" + - name: bins + desc: "Numero de bins. -1 = automatico via regla de Sturges (ImPlotBin_Sturges). Positivo = numero explicito de bins" +output: "Renderiza el histograma en el frame ImGui actual" +--- + +# histogram + +Wrapper atomico sobre `ImPlot::PlotHistogram` con seleccion automatica del numero de bins. + +Cuando `bins == -1` usa `ImPlotBin_Sturges`, que calcula el numero de bins como `ceil(log2(n)) + 1`. Para distribuciones con muchos valores o alta varianza puede preferirse pasar un valor explicito. + +El plot usa `ImVec2(-1, 0)` para ocupar el ancho disponible con altura automatica. + +Debe llamarse dentro del render callback de `fn::run_app`. diff --git a/cpp/functions/viz/kpi_card.cpp b/cpp/functions/viz/kpi_card.cpp new file mode 100644 index 00000000..516d3bb7 --- /dev/null +++ b/cpp/functions/viz/kpi_card.cpp @@ -0,0 +1,44 @@ +#include "kpi_card.h" +#include "sparkline.h" +#include +#include + +void kpi_card(const char* label, float value, float delta_percent, + const float* history, int history_count, + const char* format) { + ImGui::BeginGroup(); + + // Label — small, muted + ImGui::TextDisabled("%s", label); + + // Value — scaled up font + ImGui::SetWindowFontScale(1.8f); + char value_buf[64]; + snprintf(value_buf, sizeof(value_buf), format, value); + ImGui::Text("%s", value_buf); + ImGui::SetWindowFontScale(1.0f); + + // Delta badge — green up arrow / red down arrow + const bool positive = delta_percent >= 0.0f; + const ImVec4 delta_color = positive + ? ImVec4(0.20f, 0.80f, 0.35f, 1.0f) // green + : ImVec4(0.90f, 0.25f, 0.25f, 1.0f); // red + + char delta_buf[32]; + if (positive) { + snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xb2 +%.1f%%", delta_percent); + } else { + snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xbc %.1f%%", delta_percent); + } + + ImGui::PushStyleColor(ImGuiCol_Text, delta_color); + ImGui::Text("%s", delta_buf); + ImGui::PopStyleColor(); + + // Sparkline — matches delta color + if (history != nullptr && history_count > 0) { + sparkline(label, history, history_count, delta_color, 120.0f, 24.0f); + } + + ImGui::EndGroup(); +} diff --git a/cpp/functions/viz/kpi_card.h b/cpp/functions/viz/kpi_card.h new file mode 100644 index 00000000..1c9d461e --- /dev/null +++ b/cpp/functions/viz/kpi_card.h @@ -0,0 +1,16 @@ +#pragma once + +// KPI card — displays a key metric with trend. +// Usage: +// float history[] = {10, 12, 11, 15, 18, 17, 20}; +// kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f"); +// +// Shows: +// - Label (small, muted) +// - Value (large font) +// - Delta badge (green up / red down) +// - Sparkline of history + +void kpi_card(const char* label, float value, float delta_percent, + const float* history = nullptr, int history_count = 0, + const char* format = "%.1f"); diff --git a/cpp/functions/viz/kpi_card.md b/cpp/functions/viz/kpi_card.md new file mode 100644 index 00000000..0838f0a3 --- /dev/null +++ b/cpp/functions/viz/kpi_card.md @@ -0,0 +1,71 @@ +--- +name: kpi_card +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "void kpi_card(const char* label, float value, float delta_percent, const float* history = nullptr, int history_count = 0, const char* format = \"%.1f\")" +description: "Card de KPI con valor grande, delta porcentual, y sparkline historico para dashboards" +tags: [imgui, kpi, card, dashboard, metrics, sparkline] +uses_functions: ["sparkline_cpp_viz"] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/kpi_card.cpp" +framework: imgui +params: + - name: label + desc: "Nombre del KPI mostrado como header muted (ej: \"Revenue\", \"Users\")" + - name: value + desc: "Valor numerico actual del KPI" + - name: delta_percent + desc: "Cambio porcentual respecto al periodo anterior (positivo = mejora, negativo = deterioro)" + - name: history + desc: "Array de valores historicos para el sparkline. Nullable — si es nullptr no se renderiza sparkline" + - name: history_count + desc: "Numero de valores en el array history" + - name: format + desc: "Formato printf para el valor principal (ej: \"$%.0f\", \"%.1f%%\", \"%.2f\")" +output: "Renderiza la card KPI completa en el frame ImGui actual: label muted, valor grande, badge delta verde/rojo con triangulo, y sparkline de 120x24px" +--- + +# kpi_card + +Card compacta para dashboards ImGui que muestra un KPI con contexto de tendencia. Combina label, valor escalado, badge de delta colorizado y sparkline historico en un grupo coherente de ~150px de ancho. + +Usa `sparkline` del registry para el historico, con el mismo color que el badge (verde si delta >= 0, rojo si delta < 0). + +Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). + +## Ejemplo + +```cpp +float history[] = {10.0f, 12.0f, 11.0f, 15.0f, 18.0f, 17.0f, 20.0f}; +kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f"); + +// Sin sparkline +kpi_card("Error Rate", 0.3f, -15.2f, nullptr, 0, "%.2f%%"); + +// Grid de KPIs +ImGui::Columns(3, "kpis", false); +kpi_card("MAU", 1250000.0f, 3.4f, mau_history, 30); +ImGui::NextColumn(); +kpi_card("Revenue", 89400.0f, -1.2f, rev_history, 30, "$%.0f"); +ImGui::NextColumn(); +kpi_card("Churn", 2.1f, -0.3f, churn_history, 30, "%.1f%%"); +ImGui::Columns(1); +``` + +## Notas + +- El ancho total del grupo es aproximadamente 150px, apto para grids de 2-4 columnas. +- El escalado de fuente usa `SetWindowFontScale(1.8f)` — compatible con cualquier fuente cargada; no requiere fonts adicionales. +- Los caracteres UTF-8 del triangulo (`▲` U+25B2 y `▼` U+25BC) requieren que la fuente ImGui tenga el rango de simbolos geometricos cargado, o bien sustituir por ASCII (`^` y `v`). +- El color verde del delta es `ImVec4(0.20, 0.80, 0.35, 1.0)` y el rojo `ImVec4(0.90, 0.25, 0.25, 1.0)`, coherentes con los colores del sparkline subyacente. +- `BeginGroup`/`EndGroup` permite usar `SameLine()` despues de `kpi_card` y que el cursor avance correctamente. diff --git a/cpp/functions/viz/pie_chart.cpp b/cpp/functions/viz/pie_chart.cpp new file mode 100644 index 00000000..1b5a7406 --- /dev/null +++ b/cpp/functions/viz/pie_chart.cpp @@ -0,0 +1,31 @@ +#include "viz/pie_chart.h" +#include "implot.h" + +void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius) { + if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_Equal | ImPlotFlags_NoLegend)) { + ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations); + ImPlot::SetupAxesLimits(0, 1, 0, 1); + if (radius < 0.0f) { + // Donut mode: outer = |radius|, inner = 0.2 + ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, -radius, "%.1f", 90.0, ImPlotPieChartFlags_None); + } else { + float r = (radius > 0.0f) ? radius : 0.4f; + ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0, ImPlotPieChartFlags_None); + } + ImPlot::EndPlot(); + } +} + +void pie_chart(const char* title, const char* const* labels, const double* values, int count, double radius) { + if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_Equal | ImPlotFlags_NoLegend)) { + ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations); + ImPlot::SetupAxesLimits(0, 1, 0, 1); + if (radius < 0.0) { + ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, -radius, "%.1f", 90.0, ImPlotPieChartFlags_None); + } else { + double r = (radius > 0.0) ? radius : 0.4; + ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0, ImPlotPieChartFlags_None); + } + ImPlot::EndPlot(); + } +} diff --git a/cpp/functions/viz/pie_chart.h b/cpp/functions/viz/pie_chart.h new file mode 100644 index 00000000..9fcec19e --- /dev/null +++ b/cpp/functions/viz/pie_chart.h @@ -0,0 +1,7 @@ +#pragma once + +// Renders a pie or donut chart using ImPlot::PlotPieChart. +// Call within an ImGui frame. +// radius == 0: auto (0.4). radius > 0: explicit radius. radius < 0: donut mode (|radius| as outer, 0.2 as inner). +void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius = 0.0f); +void pie_chart(const char* title, const char* const* labels, const double* values, int count, double radius = 0.0); diff --git a/cpp/functions/viz/pie_chart.md b/cpp/functions/viz/pie_chart.md new file mode 100644 index 00000000..d6d69872 --- /dev/null +++ b/cpp/functions/viz/pie_chart.md @@ -0,0 +1,46 @@ +--- +name: pie_chart +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius = 0.0f)" +description: "Renderiza un grafico circular (pie/donut) usando ImPlot PlotPieChart dentro de un frame ImGui" +tags: [implot, chart, visualization, gpu, pie, donut] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [implot] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/pie_chart.cpp" +framework: imgui +params: + - name: title + desc: "Titulo del grafico" + - name: labels + desc: "Array de etiquetas para cada segmento del pie" + - name: values + desc: "Array de valores numericos para cada segmento" + - name: count + desc: "Numero de segmentos (longitud de labels y values)" + - name: radius + desc: "Radio del pie (0 = auto 0.4). Positivo = radio explicito. Negativo = modo donut con outer radius = |radius| e inner = 0.2" +output: "Renderiza el grafico circular en el frame ImGui actual" +--- + +# pie_chart + +Wrapper atomico sobre `ImPlot::PlotPieChart` con soporte para modo pie y modo donut. + +El eje del plot se configura con `ImPlotAxisFlags_NoDecorations` para ocultar los ejes y mostrar solo el grafico circular. El aspecto se fuerza a cuadrado con `ImPlotFlags_Equal`. + +**Modo pie** (`radius >= 0`): dibuja un pie chart solido. Si `radius == 0`, usa radio automatico de 0.4. + +**Modo donut** (`radius < 0`): usa `|radius|` como radio exterior. El agujero interior es fijo en 0.2, suficiente para texto central. + +Debe llamarse dentro del render callback de `fn::run_app`. diff --git a/cpp/functions/viz/sparkline.cpp b/cpp/functions/viz/sparkline.cpp new file mode 100644 index 00000000..ef83daa1 --- /dev/null +++ b/cpp/functions/viz/sparkline.cpp @@ -0,0 +1,76 @@ +#include "viz/sparkline.h" +#include "imgui.h" + +void sparkline(const char* id, const float* values, int count, ImVec4 color, + float width, float height) { + if (count <= 0) return; + + ImGui::PushID(id); + + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Reserve inline space + ImGui::Dummy(ImVec2(width, height)); + + // Find min/max for Y auto-scale + float min_val = values[0]; + float max_val = values[0]; + for (int i = 1; i < count; i++) { + if (values[i] < min_val) min_val = values[i]; + if (values[i] > max_val) max_val = values[i]; + } + float range = max_val - min_val; + if (range < 1e-6f) range = 1.0f; // avoid division by zero for flat lines + + // Fill area under curve (low alpha) + if (count >= 2) { + ImU32 fill_color = IM_COL32( + (int)(color.x * 255), + (int)(color.y * 255), + (int)(color.z * 255), + 40); + + // Build fill polygon: baseline bottom-left -> points -> baseline bottom-right + // We use AddConvexPolyFilled workaround: draw as a series of triangles from baseline + float x0 = pos.x; + float y_base = pos.y + height; + + for (int i = 0; i + 1 < count; i++) { + float xa = x0 + ((float)i / (count - 1)) * width; + float xb = x0 + ((float)(i + 1) / (count - 1)) * width; + float ya = pos.y + height - ((values[i] - min_val) / range) * height; + float yb = pos.y + height - ((values[i + 1] - min_val) / range) * height; + + draw_list->AddQuadFilled( + ImVec2(xa, y_base), + ImVec2(xa, ya), + ImVec2(xb, yb), + ImVec2(xb, y_base), + fill_color); + } + } + + // Draw polyline + ImU32 line_color = IM_COL32( + (int)(color.x * 255), + (int)(color.y * 255), + (int)(color.z * 255), + (int)(color.w * 255)); + + for (int i = 0; i + 1 < count; i++) { + float xa = pos.x + ((float)i / (count - 1)) * width; + float xb = pos.x + ((float)(i + 1) / (count - 1)) * width; + float ya = pos.y + height - ((values[i] - min_val) / range) * height; + float yb = pos.y + height - ((values[i + 1] - min_val) / range) * height; + draw_list->AddLine(ImVec2(xa, ya), ImVec2(xb, yb), line_color, 1.5f); + } + + ImGui::PopID(); +} + +void sparkline(const char* id, const float* values, int count, + float width, float height) { + // Default color: soft green + sparkline(id, values, count, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), width, height); +} diff --git a/cpp/functions/viz/sparkline.h b/cpp/functions/viz/sparkline.h new file mode 100644 index 00000000..4994ea24 --- /dev/null +++ b/cpp/functions/viz/sparkline.h @@ -0,0 +1,12 @@ +#pragma once + +#include "imgui.h" + +// Renders a mini inline line chart for use in tables, headers and KPI cards. +// Auto-scales Y to the min/max of values. +// Uses PushID/PopID with id for uniqueness inside tables. +void sparkline(const char* id, const float* values, int count, + float width = 100.0f, float height = 20.0f); + +void sparkline(const char* id, const float* values, int count, ImVec4 color, + float width = 100.0f, float height = 20.0f); diff --git a/cpp/functions/viz/sparkline.md b/cpp/functions/viz/sparkline.md new file mode 100644 index 00000000..74981921 --- /dev/null +++ b/cpp/functions/viz/sparkline.md @@ -0,0 +1,69 @@ +--- +name: sparkline +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "void sparkline(const char* id, const float* values, int count, float width = 100.0f, float height = 20.0f)" +description: "Renderiza un mini grafico de lineas inline para uso en tablas, headers y KPI cards" +tags: [imgui, visualization, sparkline, inline, dashboard] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/sparkline.cpp" +framework: imgui +params: + - name: id + desc: "Identificador unico del widget, usado con PushID/PopID para garantizar unicidad en tablas" + - name: values + desc: "Array de valores float del sparkline (serie temporal)" + - name: count + desc: "Numero de valores en el array" + - name: width + desc: "Ancho en pixels del sparkline (default 100.0)" + - name: height + desc: "Alto en pixels del sparkline (default 20.0)" +output: "Renderiza el sparkline inline en el frame ImGui actual, reservando espacio con ImGui::Dummy" +--- + +# sparkline + +Mini grafico de lineas inline construido sobre ImGui draw primitives. No requiere ImPlot. + +Auto-escala el eje Y al rango minimo/maximo de los valores. Dibuja una polyline con relleno semitransparente bajo la curva. Disenado para encajar en celdas de tablas, headers y tarjetas KPI. + +Ofrece dos overloads: +- Sin color: usa verde suave por defecto (`ImVec4(0.35, 0.85, 0.45, 1.0)`) +- Con color: acepta cualquier `ImVec4` para personalizar la linea y el relleno + +Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). + +## Ejemplo + +```cpp +// En una celda de tabla +ImGui::TableNextColumn(); +sparkline("##revenue_spark", revenue.data(), (int)revenue.size(), 80.0f, 18.0f); + +// Con color personalizado (rojo para valores negativos) +sparkline("##pnl", pnl.data(), (int)pnl.size(), + ImVec4(0.9f, 0.3f, 0.3f, 1.0f), 100.0f, 20.0f); + +// KPI card inline con label +ImGui::Text("Revenue"); ImGui::SameLine(); +sparkline("kpi_rev", data, count); +``` + +## Notas + +- El relleno bajo la curva usa alpha 40/255 del mismo color de la linea. +- Si todos los valores son iguales (rango < 1e-6), la linea se dibuja en el centro verticalmente. +- El grosor de linea es 1.5px para que sea legible a alturas de 16-24px. +- `id` no se muestra visualmente; solo se pasa a `PushID` para que ImGui diferencie widgets con los mismos datos en la misma tabla. diff --git a/cpp/functions/viz/surface_plot_3d.cpp b/cpp/functions/viz/surface_plot_3d.cpp new file mode 100644 index 00000000..99c2f0bd --- /dev/null +++ b/cpp/functions/viz/surface_plot_3d.cpp @@ -0,0 +1,12 @@ +#include "viz/surface_plot_3d.h" +#include "imgui.h" + +void surface_plot_3d(const char* title, const float* values, int rows, int cols, + float z_min, float z_max) { + ImGui::BeginGroup(); + ImGui::TextDisabled("[STUB] %s", title); + ImGui::TextWrapped("surface_plot_3d requires ImPlot3D. " + "Add it to cpp/vendor/implot3d/ and rebuild."); + ImGui::Text("Data: %dx%d, range [%.2f, %.2f]", rows, cols, z_min, z_max); + ImGui::EndGroup(); +} diff --git a/cpp/functions/viz/surface_plot_3d.h b/cpp/functions/viz/surface_plot_3d.h new file mode 100644 index 00000000..9e1f43c8 --- /dev/null +++ b/cpp/functions/viz/surface_plot_3d.h @@ -0,0 +1,8 @@ +#pragma once + +// [STUB] Renders a 3D surface plot using ImPlot3D. +// Requires ImPlot3D to be vendored in cpp/vendor/implot3d/. +// Until then, displays a placeholder message inside an ImGui group. +// Call within an ImGui frame (inside fn::run_app render callback). +void surface_plot_3d(const char* title, const float* values, int rows, int cols, + float z_min, float z_max); diff --git a/cpp/functions/viz/surface_plot_3d.md b/cpp/functions/viz/surface_plot_3d.md new file mode 100644 index 00000000..940765eb --- /dev/null +++ b/cpp/functions/viz/surface_plot_3d.md @@ -0,0 +1,61 @@ +--- +name: surface_plot_3d +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "void surface_plot_3d(const char* title, const float* values, int rows, int cols, float z_min, float z_max)" +description: "[STUB] Renderiza una superficie 3D — requiere ImPlot3D (no vendoreado aun)" +tags: [implot3d, chart, visualization, gpu, surface, 3d, stub] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/surface_plot_3d.cpp" +framework: imgui +params: + - name: title + desc: "Titulo de la superficie, se muestra como header del plot" + - name: values + desc: "Array row-major de alturas Z con dimension rows*cols" + - name: rows + desc: "Numero de filas de la grilla de la superficie" + - name: cols + desc: "Numero de columnas de la grilla de la superficie" + - name: z_min + desc: "Valor minimo del eje Z para escalar el colormap" + - name: z_max + desc: "Valor maximo del eje Z para escalar el colormap" +output: "Renderiza un placeholder informativo en el frame ImGui actual; cuando ImPlot3D este disponible, renderizara la superficie 3D" +--- + +# surface_plot_3d + +**STUB** — la implementacion real requiere [ImPlot3D](https://github.com/brenocq/implot3d), que todavia no esta vendoreado en `cpp/vendor/implot3d/`. + +Mientras tanto la funcion renderiza un grupo ImGui con un mensaje informativo que muestra el titulo, las dimensiones de la grilla y el rango Z. La firma es definitiva y no cambiara cuando se integre ImPlot3D. + +## Dependencia pendiente + +Para activar la implementacion real: + +1. Clonar o copiar ImPlot3D en `cpp/vendor/implot3d/` +2. Anadir `implot3d.cpp` al build system (CMake / Makefile) +3. Reemplazar el cuerpo de `surface_plot_3d` por la llamada a `ImPlot3D::BeginPlot3D` / `ImPlot3D::PlotSurface` / `ImPlot3D::EndPlot3D` +4. Actualizar `imports` del .md a `[imgui, implot3d]` y quitar el tag `stub` + +## Uso + +```cpp +// values es un array row-major de rows*cols floats +float grid[4 * 4] = { ... }; +surface_plot_3d("Mi Superficie", grid, 4, 4, -1.0f, 1.0f); +``` + +Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). diff --git a/cpp/functions/viz/table_view.cpp b/cpp/functions/viz/table_view.cpp new file mode 100644 index 00000000..f8285467 --- /dev/null +++ b/cpp/functions/viz/table_view.cpp @@ -0,0 +1,32 @@ +#include "viz/table_view.h" +#include "imgui.h" + +bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count) { + ImGuiTableFlags flags = + ImGuiTableFlags_Borders | + ImGuiTableFlags_Sortable | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_Resizable | + ImGuiTableFlags_ScrollY | + ImGuiTableFlags_Reorderable; + + if (!ImGui::BeginTable(id, col_count, flags, ImVec2(0, 300))) { + return false; + } + + for (int col = 0; col < col_count; col++) { + ImGui::TableSetupColumn(headers[col]); + } + ImGui::TableHeadersRow(); + + for (int row = 0; row < row_count; row++) { + ImGui::TableNextRow(); + for (int col = 0; col < col_count; col++) { + ImGui::TableSetColumnIndex(col); + ImGui::TextUnformatted(cells[row * col_count + col]); + } + } + + ImGui::EndTable(); + return true; +} diff --git a/cpp/functions/viz/table_view.h b/cpp/functions/viz/table_view.h new file mode 100644 index 00000000..268264d5 --- /dev/null +++ b/cpp/functions/viz/table_view.h @@ -0,0 +1,6 @@ +#pragma once + +// Renders an interactive table with sorting indicators and scroll using the ImGui Tables API. +// Call within an ImGui frame. +// Returns true if the table was rendered visible, false if clipped/skipped. +bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count); diff --git a/cpp/functions/viz/table_view.md b/cpp/functions/viz/table_view.md new file mode 100644 index 00000000..927ad8a6 --- /dev/null +++ b/cpp/functions/viz/table_view.md @@ -0,0 +1,67 @@ +--- +name: table_view +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count)" +description: "Renderiza una tabla interactiva con sorting y scroll usando ImGui Tables API" +tags: [imgui, table, visualization, dashboard, data] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/table_view.cpp" +framework: imgui +params: + - name: id + desc: "Identificador unico de la tabla para ImGui (debe ser unico en el frame)" + - name: headers + desc: "Array de strings con los nombres de las columnas" + - name: col_count + desc: "Numero de columnas" + - name: cells + desc: "Array flat row-major de strings; acceso a celda (row, col) via cells[row * col_count + col]" + - name: row_count + desc: "Numero de filas de datos, sin contar el header" +output: "true si la tabla se renderizo visible, false si fue clipped o skipped por ImGui" +--- + +# table_view + +Wrapper atomico sobre `ImGui::BeginTable` / `ImGui::EndTable`. Renderiza una tabla con las siguientes capacidades: + +- **Borders**: bordes entre celdas y columnas +- **Sortable**: muestra indicadores de orden en los headers (el caller es responsable de ordenar `cells` antes de llamar) +- **RowBg**: filas alternadas con color de fondo +- **Resizable**: el usuario puede arrastrar los separadores de columna +- **ScrollY**: scroll vertical con altura fija de 300px +- **Reorderable**: el usuario puede reordenar columnas arrastrando los headers + +El caller controla el orden de los datos — `table_view` solo habilita el flag `Sortable` para que ImGui muestre los indicadores visuales, pero no reordena `cells` internamente. + +Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). + +## Ejemplo + +```cpp +const char* headers[] = {"Nombre", "Valor", "Estado"}; +const char* cells[] = { + "Alpha", "1.23", "OK", + "Beta", "4.56", "WARN", + "Gamma", "7.89", "ERROR", +}; +table_view("##mi_tabla", headers, 3, cells, 3); +``` + +## Notas + +- `id` debe comenzar con `##` si no se quiere mostrar como titulo de ventana en el contexto ImGui. +- El outer size fijo de `ImVec2(0, 300)` puede parametrizarse en una version futura. +- El sorting real de datos queda fuera del scope de esta funcion para mantenerla pura y componible. diff --git a/cpp/types/viz/graph_types.md b/cpp/types/viz/graph_types.md new file mode 100644 index 00000000..aaf50313 --- /dev/null +++ b/cpp/types/viz/graph_types.md @@ -0,0 +1,106 @@ +--- +name: GraphData +lang: cpp +domain: viz +version: "1.0.0" +algebraic: product +definition: | + struct GraphNode { + uint32_t id; + float x, y; + float vx, vy; + float size; + uint32_t color; + const char* label; + uint32_t community; + float value; + bool pinned; + }; + + struct GraphEdge { + uint32_t source; + uint32_t target; + float weight; + uint32_t color; + }; + + struct GraphData { + GraphNode* nodes; + int node_count; + GraphEdge* edges; + int edge_count; + float min_x, min_y, max_x, max_y; + void update_bounds(); + int find_node(uint32_t id) const; + }; +description: "Tipos de datos base para el sistema de grafos GPU del registry. GraphNode modela un vertice con posicion, velocidad, apariencia y metadatos de layout. GraphEdge modela una arista con peso y color. GraphData es el contenedor principal que agrupa nodos y aristas con bounding box y metodos de consulta. Disenado para integrarse con force-directed layout y renderizado GPU via ImGui/ImPlot." +tags: [graph, network, visualization, gpu, force-layout, node, edge, imgui] +uses_types: [] +file_path: "cpp/functions/viz/graph_types.h" +--- + +## Structs + +### GraphNode + +Vertice del grafo. Contiene todos los datos necesarios para el layout y el renderizado. + +| Campo | Tipo | Descripcion | +|---|---|---| +| `id` | `uint32_t` | Identificador unico del nodo | +| `x, y` | `float` | Posicion en el espacio de layout | +| `vx, vy` | `float` | Velocidad del nodo, usada por el algoritmo force-directed | +| `size` | `float` | Radio visual en pixels (por defecto 4.0) | +| `color` | `uint32_t` | Color en formato ABGR packed. 0 = usar paleta automatica por community | +| `label` | `const char*` | Etiqueta visible. `nullptr` = sin etiqueta | +| `community` | `uint32_t` | ID de grupo/cluster para auto-coloreo. 0 = sin grupo | +| `value` | `float` | Metrica arbitraria (puede usarse para escalar el tamaño del nodo) | +| `pinned` | `bool` | Si es `true`, el force layout no mueve este nodo | + +### GraphEdge + +Arista del grafo. Referencia nodos por indice (no por id) para acceso O(1) en el loop de simulacion. + +| Campo | Tipo | Descripcion | +|---|---|---| +| `source` | `uint32_t` | Indice en `GraphData::nodes` del nodo origen | +| `target` | `uint32_t` | Indice en `GraphData::nodes` del nodo destino | +| `weight` | `float` | Peso de la arista. Afecta la fuerza de atraccion en el layout | +| `color` | `uint32_t` | Color ABGR packed. 0 = gris por defecto | + +### GraphData + +Contenedor principal. Posee los arrays de nodos y aristas (memoria gestionada externamente). Mantiene un bounding box actualizable para proyeccion de coordenadas a pantalla. + +| Campo/Metodo | Descripcion | +|---|---| +| `nodes` / `node_count` | Array de nodos y su longitud | +| `edges` / `edge_count` | Array de aristas y su longitud | +| `min_x, min_y, max_x, max_y` | Bounding box calculado sobre las posiciones actuales | +| `update_bounds()` | Recalcula el bounding box iterando todos los nodos | +| `find_node(id)` | Busqueda lineal por `GraphNode::id`. Retorna -1 si no existe | + +## Helpers + +```cpp +// Crear un nodo con valores por defecto +GraphNode n = graph_node(42, 100.0f, 200.0f); + +// Crear una arista con peso por defecto 1.0 +GraphEdge e = graph_edge(0, 1, 2.5f); +``` + +## Implementacion + +Los metodos `update_bounds()` y `find_node()` estan implementados en `cpp/functions/viz/graph_types.cpp`. + +`update_bounds()` es O(n) sobre `node_count`. Llamar despues de cada step del layout para mantener el bounding box fresco. + +`find_node()` es O(n) por busqueda lineal. Para grafos grandes (>10k nodos) considerar mantener un `unordered_map` externo como indice. + +## Notas de diseño + +- La memoria de `nodes` y `edges` es propiedad del caller. `GraphData` no hace `new`/`delete`. +- `color` usa formato ABGR packed (compatible con ImGui `ImU32`): `0xAABBGGRR`. +- Las aristas referencian por indice, no por id, para que el loop de simulacion sea cache-friendly. +- `community` con valor 0 se interpreta como "sin grupo" — los colores de comunidad empiezan desde 1.